From harness-claude
Explains HTML5 parsing algorithm—tokenization, tree construction, speculative parsing, preload scanner—to minimize parser-blocking delays and accelerate DOM construction. Use for late DOMContentLoaded, long Parse HTML in DevTools, or script loading strategies.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Understand the HTML5 parsing algorithm — tokenization, tree construction, speculative parsing, and the preload scanner — to minimize parser-blocking delays and accelerate DOM construction.
Optimizes browser's 5-stage critical rendering path (DOM/CSSOM/Render Tree/Layout/Paint) to cut time-to-first-paint by spotting render-blocking resources, measuring critical metrics, and inlining CSS. Use for blank screens, slow paints, or Lighthouse flags.
Provides expert implementation of HTML/CSS/JS, web APIs, HTTP protocols, security, performance, and accessibility following modern web standards for frontend/backend.
Optimizes website and web app performance by measuring Core Web Vitals with Lighthouse, analyzing bundle sizes and bottlenecks, and implementing caching, code splitting, and asset optimizations.
Share bugs, ideas, or general feedback.
Understand the HTML5 parsing algorithm — tokenization, tree construction, speculative parsing, and the preload scanner — to minimize parser-blocking delays and accelerate DOM construction.
document.write() to inject scripts or content dynamically<head> are blocking the parser and delaying resource discoverydefer, async, or module scripts for loading strategyDOMContentLoaded and load event timings differ significantly and you need to understand whyUnderstand the HTML5 parsing pipeline. The parser operates in four stages: byte stream decoding (charset detection), tokenization (converting characters into tokens: start tags, end tags, character data, comments), tree construction (building the DOM tree from tokens following the HTML5 algorithm's insertion modes), and script execution (when a <script> tag is encountered without defer/async).
Identify parser-blocking resources. A classic <script src="..."> tag without defer or async halts the HTML parser completely. The parser must wait for the script to download and execute before it can continue building the DOM. This is because scripts can call document.write() which modifies the token stream.
<!-- Parser-blocking: parser stops, downloads, executes, then resumes -->
<script src="/heavy-library.js"></script>
<!-- Non-parser-blocking: parser continues, script executes after parsing -->
<script src="/heavy-library.js" defer></script>
<!-- Module scripts are deferred by default -->
<script type="module" src="/app.js"></script>
Leverage the preload scanner. When the main parser is blocked on a script, browsers run a secondary "preload scanner" (also called speculative parser) that scans ahead in the raw HTML to discover resources like images, stylesheets, and other scripts. It then initiates fetches for those resources in parallel. The preload scanner typically improves page load times by 20% or more (per Chrome team measurements).
Avoid defeating the preload scanner. The preload scanner can only find resources visible in the raw HTML. Resources loaded via JavaScript, CSS background-image, or document.write() are invisible to it.
// BAD: invisible to preload scanner — discovered only when JS executes
const img = new Image();
img.src = '/hero.jpg';
// GOOD: visible to preload scanner in raw HTML
// <link rel="preload" href="/hero.jpg" as="image">
Use streaming HTML delivery. Flush the <head> and initial content as soon as possible:
// Express.js streaming example
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.write(`<!DOCTYPE html><html><head>
<link rel="stylesheet" href="/critical.css">
<link rel="preload" href="/hero.jpg" as="image">
</head><body><div id="root">`);
// Continue processing while browser fetches resources
const data = await fetchData();
res.write(renderContent(data));
res.end('</div></body></html>');
});
Measure DOM parsing performance. Use the Navigation Timing API to isolate parsing time:
const nav = performance.getEntriesByType('navigation')[0];
const parsingTime = nav.domInteractive - nav.responseEnd;
const domContentLoaded = nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart;
console.log(`DOM parsing: ${parsingTime}ms, DOMContentLoaded handler: ${domContentLoaded}ms`);
The HTML5 tokenizer is a state machine with 80+ states. It processes the character stream and emits tokens: DOCTYPE, start tag, end tag, comment, character, and end-of-file. The tree construction stage consumes these tokens and builds the DOM tree according to the HTML5 specification's precise rules for handling malformed markup (foster parenting, adoption agency algorithm, implicit tag closing).
Key performance insight: the tokenizer is fast (linear in input size), but tree construction can be expensive when the DOM tree is deep. Each node insertion requires walking up the tree to find the correct insertion point. Browsers optimize this with stack-based insertion mode tracking, but pathologically deep nesting (>1,500 levels) degrades performance.
When the main parser blocks on a synchronous script, the speculative parser takes over. It does not build a DOM tree. Instead, it scans the raw byte stream for URLs in known attributes (href, src, poster, srcset) and queues them for download. This happens on a separate thread in most modern browsers.
The speculative parser has limitations: it cannot parse JavaScript, evaluate CSS url() values, or follow redirects in meta refresh tags. It also cannot discover resources that depend on the execution of earlier scripts.
The Chrome team measured the impact of the preload scanner across 10,000 popular sites. On average, disabling the preload scanner increased page load time by 20%. On sites with many synchronous scripts in the <head>, the impact was even larger — up to 50% slower. The scanner discovers an average of 7 resources per page before the main parser reaches them, saving one or more network roundtrips.
eBay implemented streaming HTML where the server flushes the <head> section (containing critical CSS links and preload hints) within 100ms, before backend data fetching completes. The body content streams as data becomes available. This architecture reduced time-to-first-byte for visual content from 800ms to 100ms. The browser begins fetching CSS and fonts while the server is still querying databases and assembling page content.
document.write() for script injection. document.write() inserts content directly into the parser's token stream, which forces the parser to restart tokenization. On 2G connections, Chrome intervenes and blocks document.write()-injected cross-origin scripts entirely because the delay is too severe. Use document.createElement('script') and appendChild instead.
Excessive DOM depth. DOM trees deeper than 1,500 nodes cause quadratic increases in style recalculation cost. Each CSS selector match requires walking up the ancestor chain. Trees with more than 32 levels of nesting also trigger browser-specific performance cliffs in layout computation.
Parser-blocking scripts in <head> without defer/async. Every synchronous script in <head> adds download time plus execution time to the critical path before the browser can start rendering. A chain of 5 synchronous scripts, each taking 200ms to download, adds 1 second of sequential blocking even if the preload scanner discovers them early.
Injecting large HTML via innerHTML. Setting innerHTML bypasses incremental parsing. The browser must parse the entire HTML string at once, construct the subtree, and insert it. For large fragments (>10KB of HTML), this creates a noticeable jank spike. Use insertAdjacentHTML for incremental insertions or DocumentFragment for batch DOM construction.
DOMContentLoaded fires when the HTML document is fully parsed and all deferred scripts have executed, but external resources like images, stylesheets loaded asynchronously, and subframes may still be loading. The load event fires only when all dependent resources are fully loaded.
Key timing relationships:
responseEnd — HTML bytes fully receiveddomInteractive — HTML parsing complete, DOM tree builtdomContentLoadedEventStart — deferred scripts executed, ready for DOM manipulationloadEventStart — all resources loaded (images, stylesheets, subframes)The gap between domInteractive and domContentLoadedEventStart represents deferred script execution time. A large gap indicates heavy deferred JavaScript that should be code-split or lazy-loaded.
For optimal performance, target these DOM complexity budgets:
Each additional DOM node increases memory usage by approximately 0.5-1KB and adds incremental cost to style recalculation, layout, and garbage collection.