diff --git a/benchmark/react/cases/006-fuzz-complex-1000/index.tsx b/benchmark/react/cases/006-fuzz-complex-1000/index.tsx
new file mode 100644
index 0000000000..4dbbc46e45
--- /dev/null
+++ b/benchmark/react/cases/006-fuzz-complex-1000/index.tsx
@@ -0,0 +1,39 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { root, useEffect, useState } from '@lynx-js/react';
+import { process } from '@lynx-js/react/internal';
+
+import { genRandom, genValue } from '../../plugins/gen.js';
+
+const F = __GENERATE_JSX__(0, 1000)({ useState }, genRandom, genValue);
+
+function App() {
+ const [stopBenchmark, setStopBenchmark] = useState(false);
+ const [seed, setSeed] = useState(100);
+ useEffect(() => {
+ setTimeout(() => {
+ setSeed(101);
+ setStopBenchmark(true);
+ Codspeed.startBenchmark();
+ process();
+ Codspeed.stopBenchmark();
+ Codspeed.setExecutedBenchmark(
+ `${__REPO_FILEPATH__}::${__webpack_chunkname__}-preactProcess`,
+ );
+ }, 0);
+ }, []);
+ return (
+ <>
+
+
+ >
+ );
+}
+
+runAfterLoadScript(() => {
+ root.render(
+ ,
+ );
+});
diff --git a/benchmark/react/cases/006-fuzz-complex-2000/index.tsx b/benchmark/react/cases/006-fuzz-complex-2000/index.tsx
new file mode 100644
index 0000000000..2b6187f6f8
--- /dev/null
+++ b/benchmark/react/cases/006-fuzz-complex-2000/index.tsx
@@ -0,0 +1,39 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { root, useEffect, useState } from '@lynx-js/react';
+import { process } from '@lynx-js/react/internal';
+
+import { genRandom, genValue } from '../../plugins/gen.js';
+
+const F = __GENERATE_JSX__(0, 2000)({ useState }, genRandom, genValue);
+
+function App() {
+ const [stopBenchmark, setStopBenchmark] = useState(false);
+ const [seed, setSeed] = useState(100);
+ useEffect(() => {
+ setTimeout(() => {
+ setSeed(101);
+ setStopBenchmark(true);
+ Codspeed.startBenchmark();
+ process();
+ Codspeed.stopBenchmark();
+ Codspeed.setExecutedBenchmark(
+ `${__REPO_FILEPATH__}::${__webpack_chunkname__}-preactProcess`,
+ );
+ }, 0);
+ }, []);
+ return (
+ <>
+
+
+ >
+ );
+}
+
+runAfterLoadScript(() => {
+ root.render(
+ ,
+ );
+});
diff --git a/benchmark/react/lynx.config.js b/benchmark/react/lynx.config.ts
similarity index 83%
rename from benchmark/react/lynx.config.js
rename to benchmark/react/lynx.config.ts
index d9b2b81b37..19fc3f98b9 100644
--- a/benchmark/react/lynx.config.js
+++ b/benchmark/react/lynx.config.ts
@@ -6,6 +6,7 @@ import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin';
import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
import { defineConfig } from '@lynx-js/rspeedy';
+import { pluginGenJSX } from './plugins/pluginGenJSX.mjs';
import { pluginRepoFilePath } from './plugins/pluginRepoFilePath.mjs';
import { pluginScriptLoad } from './plugins/pluginScriptLoad.mjs';
@@ -45,9 +46,18 @@ export default defineConfig({
'005-load-script': [
'./cases/005-load-script/index.tsx',
],
+ '006-fuzz-complex-1000': [
+ './src/patchProfile.ts',
+ './cases/006-fuzz-complex-1000/index.tsx',
+ ],
+ '006-fuzz-complex-2000': [
+ './src/patchProfile.ts',
+ './cases/006-fuzz-complex-2000/index.tsx',
+ ],
},
},
plugins: [
+ pluginGenJSX(),
pluginRepoFilePath(),
pluginReactLynx({
enableParallelElement: false,
diff --git a/benchmark/react/package.json b/benchmark/react/package.json
index 8218d17611..25171b0b07 100644
--- a/benchmark/react/package.json
+++ b/benchmark/react/package.json
@@ -10,6 +10,8 @@
"bench:003-hello-list": "benchx_cli run dist/003-hello-list.lynx.bundle --wait-for-id=stop-benchmark-true",
"bench:004-various-update": "benchx_cli run dist/004-various-update.lynx.bundle --wait-for-id=stop-benchmark-true",
"bench:005-load-script": "benchx_cli run dist/005-load-script.lynx.bundle",
+ "bench:006-fuzz-complex-1000": "benchx_cli run dist/006-fuzz-complex-1000.lynx.bundle --wait-for-id=stop-benchmark-true",
+ "bench:006-fuzz-complex-2000": "benchx_cli run dist/006-fuzz-complex-2000.lynx.bundle --wait-for-id=stop-benchmark-true",
"build": "rspeedy build",
"dev": "rspeedy dev",
"perfetto": "pnpm run --sequential --stream --aggregate-output '/^perfetto:.*/'",
@@ -18,6 +20,8 @@
"perfetto:003-hello-list": "benchx_cli -o dist/003-hello-list.ptrace run dist/003-hello-list.lynx.bundle --wait-for-id=stop-benchmark-true",
"perfetto:004-various-update": "benchx_cli -o dist/004-various-update.ptrace run dist/004-various-update.lynx.bundle --wait-for-id=stop-benchmark-true",
"perfetto:005-load-script": "benchx_cli -o dist/005-load-script.ptrace run dist/005-load-script.lynx.bundle",
+ "perfetto:006-fuzz-complex-1000": "benchx_cli -o dist/006-fuzz-complex-1000.ptrace run dist/006-fuzz-complex-1000.lynx.bundle --wait-for-id=stop-benchmark-true",
+ "perfetto:006-fuzz-complex-2000": "benchx_cli -o dist/006-fuzz-complex-2000.ptrace run dist/006-fuzz-complex-2000.lynx.bundle --wait-for-id=stop-benchmark-true",
"test": "echo 'No tests specified'"
},
"dependencies": {
@@ -34,6 +38,7 @@
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/type-element-api": "0.0.2",
"@lynx-js/types": "3.4.11",
- "@types/react": "^18.3.23"
+ "@types/react": "^18.3.23",
+ "human-id": "4.1.1"
}
}
diff --git a/benchmark/react/plugins/gen.ts b/benchmark/react/plugins/gen.ts
new file mode 100644
index 0000000000..e087503a2b
--- /dev/null
+++ b/benchmark/react/plugins/gen.ts
@@ -0,0 +1,464 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { humanId } from 'human-id';
+
+const DYNAMIC_ATTR_RATE = 0.3;
+const DYNAMIC_ATTR_COUNT_MAX = 10;
+const DYNAMIC_TEXT_RATE = 0.7;
+const COMPONENT_RATE = 0.2;
+const COMPONENT_PROPS_RATE = 0.5;
+
+const genStr = (r: Random) => {
+ const oldRandom = Math.random;
+ try {
+ Math.random = () => r.random();
+ return humanId({ adjectiveCount: 0, addAdverb: false });
+ } finally {
+ Math.random = oldRandom;
+ }
+};
+
+const genValue = (r: Random, refVar: RefVar): string => {
+ if (refVar.type === 'string') {
+ return `"${genStr(r)}"`;
+ } else if (refVar.type === 'boolean') {
+ return pickRandomly(r, ['true', 'false']);
+ } else if (refVar.type === 'number') {
+ return ~~(2 ** 31 * r.random()) + '';
+ }
+
+ throw new Error('unreachable');
+};
+
+const genJsIdentifier = (r: Random, prefix = 'value') => {
+ const oldRandom = Math.random;
+ try {
+ Math.random = () => r.random();
+ return `${prefix}${humanId({ adjectiveCount: 1 })}`;
+ } finally {
+ Math.random = oldRandom;
+ }
+};
+
+interface RefVar {
+ id: string;
+ type: 'number' | 'string' | 'boolean';
+}
+
+interface Decl {
+ id: string;
+ code: string;
+}
+
+interface Gen {
+ code: string;
+ decls: Array;
+ refVars: Array;
+ usedComplexity: number;
+}
+
+const ATTR_TYPES = [
+ 'Attr',
+ 'Dataset',
+ 'Event',
+ 'MT_Event',
+ 'Class',
+ 'Id',
+] as const;
+
+interface Random {
+ random: () => number;
+}
+
+const pickRandomly = (r: Random, items: readonly T[]): T => {
+ return items[~~(r.random() * items.length)]!;
+};
+
+const divideRandomly = (
+ r: Random,
+ items: readonly T[],
+ p: number,
+): [T[], T[]] => {
+ const a1: T[] = [];
+ const a2: T[] = [];
+ for (const item of items) {
+ if (r.random() < p) {
+ a1.push(item);
+ } else {
+ a2.push(item);
+ }
+ }
+
+ return [a1, a2];
+};
+
+const clamp = (num: number, min: number, max: number): number => {
+ return Math.min(Math.max(num, min), max);
+};
+
+const genAttr = (r: Random): Gen & { key: string } => {
+ const refVars: RefVar[] = [];
+ const addToRefVars = (type: RefVar['type']): string => {
+ const id = genJsIdentifier(r);
+ refVars.push({ id, type });
+ return id;
+ };
+
+ const isDynamic = r.random() < DYNAMIC_ATTR_RATE;
+ const attrType = pickRandomly(r, [...ATTR_TYPES, 'Attr', 'Attr', 'Attr']);
+
+ switch (attrType) {
+ case 'Attr': {
+ const key = `attr-${genStr(r)}`;
+ if (isDynamic) {
+ return {
+ code: `${key}={${addToRefVars('string')}}`,
+ refVars,
+ decls: [],
+ usedComplexity: 2,
+ key,
+ };
+ } else {
+ return {
+ code: `${key}="${genStr(r)}"`,
+ refVars: [],
+ decls: [],
+ usedComplexity: 1,
+ key,
+ };
+ }
+ }
+ case 'Dataset': {
+ const key = `data-${genStr(r)}`;
+ if (isDynamic) {
+ return {
+ code: `${key}={${addToRefVars('string')}}`,
+ refVars,
+ decls: [],
+ usedComplexity: 2,
+ key,
+ };
+ } else {
+ return {
+ code: `${key}="${genStr(r)}"`,
+ refVars: [],
+ decls: [],
+ usedComplexity: 1,
+ key,
+ };
+ }
+ }
+ case 'Id': {
+ if (isDynamic) {
+ return {
+ code: `id={${addToRefVars('string')}}`,
+ refVars,
+ decls: [],
+ usedComplexity: 2,
+ key: 'id',
+ };
+ } else {
+ return {
+ code: `id="${genStr(r)}"`,
+ refVars: [],
+ decls: [],
+ usedComplexity: 1,
+ key: 'id',
+ };
+ }
+ }
+ case 'Event': {
+ const eventType = pickRandomly(
+ r,
+ [
+ 'bind',
+ 'catch',
+ 'capture-bind',
+ 'capture-catch',
+ 'global-bind',
+ ] as const,
+ );
+ const key = `${eventType}${genStr(r)}`;
+ return {
+ code: `${key}={() => {}}`,
+ refVars: [],
+ decls: [],
+ usedComplexity: 2,
+ key,
+ };
+ }
+ case 'MT_Event': {
+ const eventType = pickRandomly(
+ r,
+ [
+ 'bind',
+ 'catch',
+ 'capture-bind',
+ 'capture-catch',
+ 'global-bind',
+ ] as const,
+ );
+ const key = `main-thread:${eventType}${genStr(r)}`;
+ return {
+ code: `${key}={() => { 'main-thread' }}`,
+ refVars: [],
+ decls: [],
+ usedComplexity: 2,
+ key,
+ };
+ }
+ case 'Class':
+ if (isDynamic) {
+ return {
+ code: `className={${addToRefVars('string')}}`,
+ refVars,
+ decls: [],
+ usedComplexity: 2,
+ key: 'className',
+ };
+ } else {
+ return {
+ code: `className="${genStr(r)}"`,
+ refVars: [],
+ decls: [],
+ usedComplexity: 1,
+ key: 'className',
+ };
+ }
+ default:
+ throw new Error('unreachable');
+ }
+};
+
+const genFC = (r: Random, complexity: number, inner?: string): Gen => {
+ let usedComplexity = 0;
+
+ complexity -= 1;
+ usedComplexity += 1;
+
+ const fcName = genJsIdentifier(r, '');
+
+ if (complexity <= 0) {
+ return {
+ code: `<${fcName} />`,
+ decls: [{
+ code: `const ${fcName} = () => null`,
+ id: fcName,
+ }],
+ refVars: [],
+ usedComplexity,
+ };
+ }
+
+ if (inner) {
+ const jsx1 = genJSX(r, ~~(complexity / 2), 1);
+ const jsx2 = genJSX(r, ~~(complexity / 2), 1);
+ const [refVars, refStates] = divideRandomly(r, [
+ ...jsx1.refVars,
+ ...jsx2.refVars,
+ ], COMPONENT_PROPS_RATE);
+ usedComplexity += jsx1.usedComplexity + jsx2.usedComplexity;
+ return {
+ code: `<${fcName} ${
+ refVars.map((v) => `${v.id}={${v.id}}`).join(' ')
+ }>${inner}${fcName}>`,
+ refVars,
+ decls: [...jsx1.decls, ...jsx2.decls, {
+ code: `const ${fcName} = ({ children, ${
+ refVars.map(v => v.id).join(', ')
+ } }) => { ${
+ refStates.map(v => `const [${v.id},] = useState(${genValue(r, v)});`)
+ .join(
+ '',
+ )
+ } return (${
+ pickRandomly(r, [
+ () => `{children}${jsx1.code}${jsx2.code}`,
+ () => `${jsx1.code}{children}${jsx2.code}`,
+ () => `${jsx1.code}${jsx2.code}{children}`,
+ ])()
+ }) }`,
+ id: fcName,
+ }],
+ usedComplexity,
+ };
+ } else {
+ const jsx = genJSX(r, complexity, 0);
+ const [refVars, refStates] = divideRandomly(
+ r,
+ jsx.refVars,
+ COMPONENT_PROPS_RATE,
+ );
+ usedComplexity += jsx.usedComplexity;
+ return {
+ code: `<${fcName} ${
+ refVars.map((v) => `${v.id}={${v.id}}`).join(' ')
+ } />`,
+ refVars,
+ decls: [...jsx.decls, {
+ code: `const ${fcName} = ({ ${
+ refVars.map(v => v.id).join(', ')
+ } }) => { ${
+ refStates.map(v => `const [${v.id},] = useState(${genValue(r, v)});`)
+ .join(
+ '',
+ )
+ } return (${jsx.code}) }`,
+ id: fcName,
+ }],
+ usedComplexity,
+ };
+ }
+};
+
+const genJSX = (r: Random, complexity: number, depth: number): Gen => {
+ if (complexity <= 0) {
+ return {
+ code: '',
+ decls: [],
+ refVars: [],
+ usedComplexity: 0,
+ };
+ }
+
+ let usedComplexity = 0;
+ let code = '';
+
+ // const tag = pickRandomly(r, ['view', 'text']);
+ const tag = 'view';
+ complexity -= 1;
+ usedComplexity += 1;
+
+ code += `<${tag}`;
+
+ const refVars: RefVar[] = [];
+ const decls: Decl[] = [];
+
+ if (complexity > 0) {
+ const attrCount = clamp(
+ ~~(r.random() * DYNAMIC_ATTR_COUNT_MAX),
+ 0,
+ ~~(complexity / 2),
+ );
+ const attrKeys = new Set();
+ for (let i = 0; i < attrCount;) {
+ const attr = genAttr(r);
+ if (attrKeys.has(attr.key)) {
+ continue;
+ }
+
+ i++;
+ code += ` ${attr.code}`;
+ refVars.push(...attr.refVars);
+ attrKeys.add(attr.key);
+ complexity -= attr.usedComplexity;
+ usedComplexity += attr.usedComplexity;
+ if (complexity <= 0) {
+ complexity = 0;
+ break;
+ }
+ }
+ }
+
+ code += '>';
+
+ if (complexity > 0) {
+ while (true) {
+ // The more deeper the nesting, the more likely to generate a
+ // ending (a Function Component without children or a text node)
+ if (r.random() > 1 / 2 ** depth) {
+ // generate a ending
+ if (r.random() < COMPONENT_RATE) {
+ const fc = genFC(r, complexity, undefined);
+ code += fc.code;
+ refVars.push(...fc.refVars);
+ decls.push(...fc.decls);
+ complexity -= fc.usedComplexity;
+ usedComplexity += fc.usedComplexity;
+ } else {
+ if (r.random() < DYNAMIC_TEXT_RATE && complexity > 2) {
+ const id = genJsIdentifier(r);
+ code += `{${id}}`;
+ refVars.push({ id, type: 'string' });
+ complexity -= 2;
+ usedComplexity += 2;
+ } else if (complexity > 1) {
+ code += `${genStr(r)}`;
+ complexity -= 1;
+ usedComplexity += 1;
+ }
+ }
+ } else {
+ // generate a nesting
+ const jsx = genJSX(r, ~~(complexity * r.random()), depth + 1);
+ complexity -= jsx.usedComplexity;
+ usedComplexity += jsx.usedComplexity;
+ if (r.random() < COMPONENT_RATE) {
+ const fc = genFC(r, complexity, jsx.code);
+ code += fc.code;
+ refVars.push(...fc.refVars, ...jsx.refVars);
+ decls.push(...fc.decls, ...jsx.decls);
+ complexity -= fc.usedComplexity;
+ usedComplexity += fc.usedComplexity;
+ } else {
+ code += jsx.code;
+ refVars.push(...jsx.refVars);
+ decls.push(...jsx.decls);
+ }
+ }
+
+ if (complexity <= 0) {
+ break;
+ }
+ }
+ }
+
+ code += `${tag}>`;
+
+ return {
+ code,
+ refVars,
+ decls,
+ usedComplexity,
+ };
+};
+
+const genRandom = function(randomSeed: number) {
+ let seed = randomSeed;
+ return function() {
+ // Robert Jenkins' 32 bit integer hash function.
+ seed = ((seed + 0x7ed55d16) + (seed << 12)) & 0xffffffff;
+ seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff;
+ seed = ((seed + 0x165667b1) + (seed << 5)) & 0xffffffff;
+ seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff;
+ seed = ((seed + 0xfd7046c5) + (seed << 3)) & 0xffffffff;
+ seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff;
+ return (seed & 0xfffffff) / 0x10000000;
+ };
+};
+
+const gen = (randomSeed: number, complexity: number): string => {
+ const r = {
+ random: genRandom(randomSeed),
+ };
+
+ const g = genJSX(r, complexity, 0);
+
+ return `(({useState}, genRandom, genValue) => {
+${g.decls.map(d => d.code).join('\n')}
+return ({seed}) => {
+ const r = { random: genRandom(seed) }
+ ${
+ g.refVars.map(v => `const ${v.id} = genValue(r, { type: "${v.type}" });`)
+ .join(
+ '\n',
+ )
+ }
+ return (${g.code})
+};
+})`;
+};
+
+export { gen, genValue, genRandom };
diff --git a/benchmark/react/plugins/pluginGenJSX.mjs b/benchmark/react/plugins/pluginGenJSX.mjs
new file mode 100644
index 0000000000..1ad2ea13c0
--- /dev/null
+++ b/benchmark/react/plugins/pluginGenJSX.mjs
@@ -0,0 +1,35 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import MagicString from 'magic-string';
+
+import { gen } from './gen.js';
+
+/**
+ * @returns {import("@lynx-js/rspeedy").RsbuildPlugin}
+ */
+export const pluginGenJSX = () => ({
+ name: 'pluginGenJSX',
+ /**
+ * @param {import("@lynx-js/rspeedy").RsbuildPluginAPI} api
+ */
+ setup(api) {
+ api.transform({}, (context) => {
+ const code = new MagicString(context.code);
+ code.replace(/__GENERATE_JSX__\((\d+), ?(\d+)\)/g, (_, $1, $2) => {
+ return gen(parseInt($1, 10), parseInt($2, 10));
+ });
+ const sourceMap = code.generateMap({
+ hires: true,
+ includeContent: true,
+ source: context.resourcePath,
+ });
+
+ return {
+ code: code.toString(),
+ map: sourceMap,
+ };
+ });
+ },
+});
diff --git a/benchmark/react/rspeedy-env.d.ts b/benchmark/react/rspeedy-env.d.ts
index b840e7dccf..f92a681117 100644
--- a/benchmark/react/rspeedy-env.d.ts
+++ b/benchmark/react/rspeedy-env.d.ts
@@ -21,3 +21,11 @@ declare const Codspeed: {
declare function runAfterLoadScript(cb: () => void): void;
declare const __REPO_FILEPATH__: string;
+declare const __GENERATE_JSX__: (
+ seed: number,
+ complexity: number,
+) => (
+ apis: Partial,
+ genRandom: typeof import('./plugins/gen.ts').genRandom,
+ genValue: typeof import('./plugins/gen.ts').genValue,
+) => import('react').FC<{ seed: number }>;
diff --git a/benchmark/react/scripts/tryGen.ts b/benchmark/react/scripts/tryGen.ts
new file mode 100644
index 0000000000..616df18ae0
--- /dev/null
+++ b/benchmark/react/scripts/tryGen.ts
@@ -0,0 +1,18 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+/* eslint-disable n/file-extension-in-import */
+
+import { readFileSync } from 'node:fs';
+
+import { gen } from '../plugins/gen.ts';
+
+console.log(
+ readFileSync(0).toString().replace(
+ /__GENERATE_JSX__\((\d+), ?(\d+)\)/g,
+ (_, $1: string, $2: string) => {
+ return gen(parseInt($1, 10), parseInt($2, 10));
+ },
+ ),
+);
diff --git a/benchmark/react/tsconfig.json b/benchmark/react/tsconfig.json
index de35ffbbd9..00ff35a1f7 100644
--- a/benchmark/react/tsconfig.json
+++ b/benchmark/react/tsconfig.json
@@ -8,11 +8,14 @@
"allowJs": true,
"checkJs": true,
"isolatedDeclarations": false,
+ "allowImportingTsExtensions": true,
},
"include": [
"rspeedy-env.d.ts",
"src",
- "lynx.config.js",
+ "plugins",
+ "scripts",
+ "lynx.config.ts",
"cases",
"vitest.config.ts",
],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 847c86d988..70c18993bf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -185,6 +185,9 @@ importers:
'@types/react':
specifier: ^18.3.23
version: 18.3.23
+ human-id:
+ specifier: 4.1.1
+ version: 4.1.1
examples/react:
dependencies: