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 = '';
}
}
replaceChildren(): void {
this.childNodes = [];
this.ownTextContent = '';
}
cloneNode(_deep: boolean): FakeElement {
return new FakeElement(this.tagName);
}
}
function installFakeDocument() {

View File

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