perf: use cloneNode template and replaceChildren for DOM rendering

Replace createElement('span') with cloneNode(false) from a lazily
initialized template span. Replace innerHTML='' with replaceChildren()
to avoid HTML parser invocation on clear. Add cloneNode/replaceChildren
to FakeElement in tests to support the new APIs.
This commit is contained in:
2026-03-15 12:52:56 -07:00
parent 047b349d05
commit d71e9e841e
2 changed files with 22 additions and 5 deletions

View File

@@ -90,6 +90,15 @@ class FakeElement {
this.ownTextContent = ''; this.ownTextContent = '';
} }
} }
replaceChildren(): void {
this.childNodes = [];
this.ownTextContent = '';
}
cloneNode(_deep: boolean): FakeElement {
return new FakeElement(this.tagName);
}
} }
function installFakeDocument() { function installFakeDocument() {

View File

@@ -19,6 +19,14 @@ export type SubtitleTokenHoverRange = {
tokenIndex: number; tokenIndex: number;
}; };
let _spanTemplate: HTMLSpanElement | null = null;
function getSpanTemplate(): HTMLSpanElement {
if (!_spanTemplate) {
_spanTemplate = document.createElement('span');
}
return _spanTemplate;
}
export function shouldRenderTokenizedSubtitle(tokenCount: number): boolean { export function shouldRenderTokenizedSubtitle(tokenCount: number): boolean {
return tokenCount > 0; return tokenCount > 0;
} }
@@ -286,7 +294,7 @@ function renderWithTokens(
} }
const token = segment.token; const token = segment.token;
const span = document.createElement('span'); const span = getSpanTemplate().cloneNode(false) as HTMLSpanElement;
span.className = computeWordClass(token, resolvedTokenRenderSettings); span.className = computeWordClass(token, resolvedTokenRenderSettings);
span.textContent = token.surface; span.textContent = token.surface;
span.dataset.tokenIndex = String(segment.tokenIndex); span.dataset.tokenIndex = String(segment.tokenIndex);
@@ -322,7 +330,7 @@ function renderWithTokens(
continue; continue;
} }
const span = document.createElement('span'); const span = getSpanTemplate().cloneNode(false) as HTMLSpanElement;
span.className = computeWordClass(token, resolvedTokenRenderSettings); span.className = computeWordClass(token, resolvedTokenRenderSettings);
span.textContent = surface; span.textContent = surface;
span.dataset.tokenIndex = String(index); span.dataset.tokenIndex = String(index);
@@ -478,7 +486,7 @@ function renderCharacterLevel(root: HTMLElement, text: string): void {
fragment.appendChild(document.createElement('br')); fragment.appendChild(document.createElement('br'));
continue; continue;
} }
const span = document.createElement('span'); const span = getSpanTemplate().cloneNode(false) as HTMLSpanElement;
span.className = 'c'; span.className = 'c';
span.textContent = char; span.textContent = char;
fragment.appendChild(span); fragment.appendChild(span);
@@ -503,7 +511,7 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void
export function createSubtitleRenderer(ctx: RendererContext) { export function createSubtitleRenderer(ctx: RendererContext) {
function renderSubtitle(data: SubtitleData | string): void { function renderSubtitle(data: SubtitleData | string): void {
ctx.dom.subtitleRoot.innerHTML = ''; ctx.dom.subtitleRoot.replaceChildren();
let text: string; let text: string;
let tokens: MergedToken[] | null; let tokens: MergedToken[] | null;
@@ -552,7 +560,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
} }
function renderSecondarySub(text: string): void { function renderSecondarySub(text: string): void {
ctx.dom.secondarySubRoot.innerHTML = ''; ctx.dom.secondarySubRoot.replaceChildren();
if (!text) return; if (!text) return;
const normalized = text const normalized = text