fix(lint): locate findings in linear time on large compositions (#4373)

Since the source-location mapping landed, building the whitespace-collapsed copy of a file grew a
string with += and called endsWith on it per character. V8 flattens that string on every call, so a
1.2 MB index.html cost ~31 s of CPU in every lint run, including the one preview runs before it
listens.
This commit is contained in:
Miguel Ángel
2026-09-23 19:39:08 -04:00
committed by GitHub
parent 0aa3cb82aa
commit 54cb982ea2
2 changed files with 17 additions and 7 deletions
@@ -201,3 +201,12 @@ it("uses classic-script Acorn semantics for new.target and hashbang", async () =
const hashbang = await lintHyperframeHtml("<script>#!/usr/bin/env node\nconst a=1;</script>");
expect(hashbang.findings.some((f) => f.code === "invalid_inline_script_syntax")).toBe(false);
});
it("locates a snippet at the end of a large source in linear time", () => {
const filler = "<div>\n <p>a b</p>\n</div>\n".repeat(12_000);
const html = `${filler}<style>\n .late {\n position: fixed;\n }\n</style>`;
const start = performance.now();
const location = sourceLocationFor(html, finding({ snippet: ".late { position: fixed; }" }));
expect(location).toEqual({ line: 36_002, column: 3 });
expect(performance.now() - start).toBeLessThan(1_000);
});
+8 -7
View File
@@ -5,17 +5,18 @@ import { parseHtmlStructure, readDecodedAttr } from "./utils";
type Location = Pick<HyperframeLintFinding, "line" | "column">;
function normalized(source: string): { text: string; offsets: number[] } {
let text = "";
// An array, not +=: V8 flattens a += string on every endsWith, which made this quadratic.
const chars: string[] = [];
const offsets: number[] = [];
let lastWasSpace = false;
for (let i = 0; i < source.length; i++) {
const char = source[i]!;
if (/\s/.test(char)) {
if (text.endsWith(" ")) continue;
text += " ";
} else text += char;
const isSpace = /\s/.test(source[i]!);
if (isSpace && lastWasSpace) continue;
chars.push(isSpace ? " " : source[i]!);
offsets.push(i);
lastWasSpace = isSpace;
}
return { text, offsets };
return { text: chars.join(""), offsets };
}
/** Resolve against original bytes, not the comment-stripped/template-unwrapped rule input.