perf(channels): the row menu opens and closes without re-rendering the table
The React Compiler silently skipped Channels and DataTable (a ref written during render; the refs lint is off), and even once compiled it dropped the memo of the rows and columns (they reach filterRows/optionsFromColumns past the page's hooks) and the columns read the open row. Every open, close and reposition of the right-click menu rebuilt the whole table: on "All" rows ~0.9 s to open and ~0.6 s to close (dev). Now: effect events instead of the render-written refs, explicit memos for rows, columns and their inputs, a module-scope keyOf, and the open row reaches its "…" through a small store. Measured on 397 rows (dev): ~75 ms to open, ~35 ms to close. A contract test runs the real compiler over both files and fails on any skipped function. The dropped memos have no automated guard: they need the full page rendered with its providers; the measurement above is the evidence.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { transformSync } from "@babel/core";
|
||||
import reactCompiler from "babel-plugin-react-compiler";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// The React Compiler bails out of a component it cannot prove safe WITHOUT a word: the component
|
||||
// still works, it just re-renders everything under it on every render. Its main tripwire, a ref read
|
||||
// or written during render, is not linted here (`react-hooks/refs` is off, see eslint.config.js), so
|
||||
// nothing caught it. The channel manager is where it hurt (the owner, 2026-10-03): Channels and
|
||||
// DataTable both bailed out, every open, close and reposition of a row's right-click menu rebuilt
|
||||
// the rows and columns, and on "All" rows the menu took ~0.9 s to open and ~0.6 s to close.
|
||||
//
|
||||
// This runs the real compiler (the options reactPlugin.ts gives it) over the files that must stay
|
||||
// compiled and fails on any function it skips, with the compiler's own reason.
|
||||
const MUST_COMPILE = ["components/Channels.tsx", "components/ui/DataTable.tsx"];
|
||||
|
||||
function bailouts(path: string): string[] {
|
||||
const source = readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
||||
const found: string[] = [];
|
||||
transformSync(source, {
|
||||
filename: path,
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
parserOpts: { plugins: ["jsx", "typescript"] },
|
||||
plugins: [
|
||||
[
|
||||
reactCompiler,
|
||||
{
|
||||
target: "19",
|
||||
logger: {
|
||||
logEvent: (
|
||||
_file: string | null,
|
||||
e: { kind: string; fnLoc?: unknown; detail?: unknown },
|
||||
) => {
|
||||
if (
|
||||
e.kind !== "CompileError" &&
|
||||
e.kind !== "CompileSkip" &&
|
||||
e.kind !== "PipelineError"
|
||||
)
|
||||
return;
|
||||
const loc = e.fnLoc as { start?: { line?: number } } | undefined;
|
||||
const d = e.detail as
|
||||
| { options?: { reason?: string; details?: { message?: string }[] } }
|
||||
| { reason?: string; details?: { message?: string }[] }
|
||||
| undefined;
|
||||
const o =
|
||||
d && "options" in d
|
||||
? d.options
|
||||
: (d as { reason?: string; details?: { message?: string }[] });
|
||||
found.push(
|
||||
`line ${loc?.start?.line}: ${o?.details?.[0]?.message ?? o?.reason ?? e.kind}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
describe("the React Compiler", () => {
|
||||
it.each(MUST_COMPILE)("compiles every function in %s", (path) => {
|
||||
expect(bailouts(path)).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user