refactor(benchmark): use react-router instead of just vite

This commit is contained in:
2025-07-14 21:39:50 +02:00
parent f52f7312a4
commit 9d175e31e8
13 changed files with 781 additions and 170 deletions

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@
/pkgs/*/.turbo/
/pkgs/solver-wasm/target/
/pkgs/benchmark/.react-router/

View File

@@ -0,0 +1,91 @@
import * as powCaptcha from "@pow-captcha/pow-captcha";
import { useCallback, useState } from "react";
import type { Route } from "./+types/App";
export async function loader({ params }: Route.LoaderArgs) {
const challenge = await powCaptcha.server.createChallengesRaw({
difficultyBits: 19,
challengeCount: 64,
});
return { challenge };
}
export default function App({
loaderData: { challenge },
}: Route.ComponentProps) {
const [durationJs, setDurationJs] = useState<null | string>(null);
const [durationWasm, setDurationWasm] = useState<null | string>(null);
const run = useCallback(
async (
engine: "wasm" | "js",
setter: React.Dispatch<React.SetStateAction<string | null>>,
) => {
try {
setter(`Running`);
const challenges = challenge.challenges.map(
(challenge) =>
[
powCaptcha.wire.deserializeArray(challenge[0]),
powCaptcha.wire.deserializeArray(challenge[1]),
] as const,
);
const start = performance.now();
await powCaptcha.solver.solveChallenges({
difficultyBits: challenge.difficultyBits,
challenges,
engine,
});
const duration = performance.now() - start;
setter(`${duration.toFixed(1)}ms`);
} catch (err) {
setter(`Error: ${err}`);
}
},
[challenge],
);
const runJs = useCallback(async () => {
await run("js", setDurationJs);
}, [run]);
const runWasm = useCallback(async () => {
await run("wasm", setDurationWasm);
}, [run]);
return (
<div>
<h1>pow-captcha Benchmark</h1>
Results:
<table>
<thead>
<tr>
<th>type</th>
<th>duration</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>wasm</td>
<td>{durationWasm}</td>
<td>
<button onClick={runWasm}>run</button>
</td>
</tr>
<tr>
<td>js</td>
<td>{durationJs}</td>
<td>
<button onClick={runJs}>run</button>
</td>
</tr>
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Outlet, Scripts } from "react-router";
export default function App() {
return (
<html lang="en">
<head>
<title>pow-captcha Benchmark</title>
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}

View File

@@ -0,0 +1,3 @@
import { type RouteConfig, index } from "@react-router/dev/routes";
export default [index("./App.tsx")] satisfies RouteConfig;

View File

@@ -3,17 +3,17 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"dev": "react-router dev",
"build": "react-router build",
"check": "react-router typegen && tsc",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.5.0",
"@eslint/js": "^9.30.1",
"@pow-captcha/pow-captcha": "workspace:*",
"@react-router/dev": "^7.6.3",
"@react-router/node": "^7.6.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
@@ -21,9 +21,14 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"isbot": "^5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.3",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4",
"@pow-captcha/pow-captcha": "workspace:*"
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.5.0"
}
}

View File

@@ -0,0 +1,5 @@
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;

View File

@@ -1,100 +0,0 @@
import * as powCaptcha from "@pow-captcha/pow-captcha";
import { useCallback, useEffect, useState } from "react";
async function benchmark(cb: () => void | Promise<void>): Promise<number> {
const start = performance.now();
await cb();
const end = performance.now();
return end - start;
}
export default function App() {
const [challenge, setChallenge] =
useState<null | powCaptcha.wire.Challenge>(null);
const [durationJs, setDurationJs] = useState<null | string>(null);
const [durationWasm, setDurationWasm] = useState<null | string>(null);
useEffect(() => {
async function init() {
const challengesRaw = await powCaptcha.server.createChallengesRaw({
difficultyBits: 19,
challengeCount: 64,
});
setChallenge(challengesRaw);
}
init();
}, []);
const run = useCallback(
async (
engine: "wasm" | "js",
setter: React.Dispatch<React.SetStateAction<string | null>>,
) => {
try {
if (!challenge) {
return;
}
setter(`Running`);
const challenges = challenge.challenges.map(
(challenge) =>
[
powCaptcha.wire.deserializeArray(challenge[0]),
powCaptcha.wire.deserializeArray(challenge[1]),
] as const,
);
const duration = await benchmark(async () => {
await powCaptcha.solver.solveChallenges({
difficultyBits: challenge.difficultyBits,
challenges,
engine,
});
});
setter(`${duration.toFixed(1)}ms`);
} catch (err) {
setter(`Error: ${err}`);
}
},
[challenge],
);
const runJs = useCallback(async () => {
await run("js", setDurationJs);
}, [run]);
const runWasm = useCallback(async () => {
await run("wasm", setDurationWasm);
}, [run]);
return (
<div>
<h1>pow-captcha Benchmark</h1>
Results:
<table>
<tr>
<th>type</th>
<th>duration</th>
<th></th>
</tr>
<tr>
<td>wasm</td>
<td>{durationWasm}</td>
<td>
<button onClick={runWasm}>run</button>
</td>
</tr>
<tr>
<td>js</td>
<td>{durationJs}</td>
<td>
<button onClick={runJs}>run</button>
</td>
</tr>
</table>
</div>
);
}

View File

@@ -1,9 +0,0 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -1,27 +1,30 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"compilerOptions": {
"types": ["@react-router/node", "vite/client"],
"rootDirs": [".", "./.react-router/types"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["app", ".react-router/types/**/*"]
}

View File

@@ -1,25 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,18 +1,12 @@
import { defineConfig, type PluginOption } from "vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
const plugins: () => Array<PluginOption> = () => [
react(),
wasm(),
topLevelAwait(),
];
// https://vite.dev/config/
export default defineConfig({
plugins: plugins(),
plugins: [reactRouter(), wasm(), topLevelAwait()],
worker: {
plugins,
plugins: () => [wasm(), topLevelAwait()],
},
});

602
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff