Every performance conversation eventually reaches the rendering pipeline: what blocked the first paint, why the layout shifted, why a click took 400 ms to respond. Interviewers ask "what happens when the HTML arrives" to see whether you know the order of operations well enough to reason about a waterfall or a profile.
media attributes make stylesheets non-blocking when they do not match; inlining critical CSS is the trade-off for very large stylesheets.<script> blocks parsing while it downloads and executes; defer downloads in parallel and executes in order after parsing; async executes as soon as it arrives, out of order; type="module" is deferred by default and strict. Nuxt emits module scripts with modulepreload hints.requestAnimationFrame callbacks → style (compute styles) → layout (geometry; "reflow") → paint (rasterise into layers) → composite (GPU assembles layers). transform and opacity changes can skip layout and paint (compositor-only); width, top, font-size force layout; color, background force paint. The frame budget at 60 Hz is about 16 ms, less on 120 Hz displays.offsetHeight, getBoundingClientRect(), getComputedStyle()) after a write forces layout immediately; alternating reads and writes in a loop forces it repeatedly. Batch reads, then writes; use requestAnimationFrame for visual updates; ResizeObserver instead of polling sizes.scheduler.yield(), defer non-urgent work (requestIdleCallback, setTimeout), avoid layout thrashing in handlers, reduce DOM size, move CPU work to Web Workers (postMessage, structuredClone, transferables; OffscreenCanvas for drawing off-thread).IntersectionObserver (lazy loading, infinite scroll, visibility-based hydration, analytics), ResizeObserver (element size changes, container-like logic in JS), MutationObserver (DOM changes; runs as a microtask), PerformanceObserver (long tasks, LCP candidates, layout shifts, event timing for INP, resource timing).content-visibility: auto and contain limit layout scope for long pages.web-vitals) at the 75th percentile is what counts; Lighthouse is a lab.experimental.ssrStreaming flushes the shell earlier; NuxtLink prefetch and Speculation Rules move navigation cost ahead of the click.Loading attributes and hints that decide the critical path:
<head>
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high"> <!-- LCP image found early -->
<link rel="stylesheet" href="/_nuxt/entry.css"> <!-- render-blocking, small -->
<link rel="stylesheet" href="/_nuxt/print.css" media="print"> <!-- non-blocking -->
<link rel="modulepreload" href="/_nuxt/entry.js">
<script type="module" src="/_nuxt/entry.js"></script> <!-- deferred by default -->
<script async src="https://analytics.example.com/tag.js"></script> <!-- independent, out of order -->
</head>
Layout thrashing and its fix, the way it appears in a real component:
export function useEqualHeights(items: Ref<HTMLElement[]>) {
// ✗ read, write, read, write: each write invalidates layout and the next read forces it again
// for (const el of items.value) { el.style.height = ''; el.style.height = `${Math.max(...items.value.map(i => i.offsetHeight))}px` }
// ✓ batch: all reads, then all writes, inside a frame
function equalise() {
requestAnimationFrame(() => {
for (const el of items.value) el.style.height = '' // writes (reset)
const max = Math.max(...items.value.map(el => el.offsetHeight)) // reads, one forced layout
for (const el of items.value) el.style.height = `${max}px` // writes
})
}
// ResizeObserver instead of a resize listener + polling
useResizeObserver(items, equalise)
onMounted(equalise)
}
Measuring what the user experiences, in the browser, and moving CPU work off the main thread:
import { onCLS, onINP, onLCP } from 'web-vitals/attribution'
export default defineNuxtPlugin(() => {
const send = (m: { name: string, value: number, attribution: Record<string, unknown> }) =>
navigator.sendBeacon('/api/telemetry/vitals', JSON.stringify({ name: m.name, value: m.value, target: m.attribution.interactionTarget ?? m.attribution.element, path: location.pathname }))
onLCP(send)
onINP(send) // attribution tells you *which* interaction and element was slow
onCLS(send)
})
// A dedicated module worker: heavy filtering off the main thread
self.onmessage = (e: MessageEvent<{ rows: Row[], query: string }>) => {
const q = e.data.query.toLowerCase()
const hits = e.data.rows.filter(r => r.name.toLowerCase().includes(q))
postMessage(hits)
}
export function useWorkerSearch(rows: Ref<Row[]>) {
const results = shallowRef<Row[]>([])
let worker: Worker | undefined
onMounted(() => { worker = new Worker(new URL('~/workers/search.worker.ts', import.meta.url), { type: 'module' }); worker.onmessage = e => { results.value = e.data } })
onScopeDispose(() => worker?.terminate())
const search = (query: string) => worker?.postMessage({ rows: toRaw(rows.value), query }) // structured clone; toRaw avoids cloning the Proxy
return { results, search }
}
async runs as soon as the script arrives, possibly before the DOM it needs exists and out of order relative to its dependencies. Application entry points are defer or type="module"; async is for independent third-party tags.
A template helper that calls getBoundingClientRect() for each row while the list renders forces a layout per row: 1,000 rows, 1,000 layouts, one long task. Measure once after render with a ResizeObserver or requestAnimationFrame, or avoid measuring by using CSS (grid, aspect-ratio).
The current Core Web Vitals thresholds and whether Chrome has announced any new or changed metric for 2026; the set was LCP, INP and CLS as of this snapshot.
docs ↗useEqualHeights with 500 cards, observe the purple layout blocks in the profile, then batch the reads and writes."The browser tokenises the HTML into a DOM incrementally while a preload scanner races ahead to fetch stylesheets, scripts, images and fonts it sees. Blocking stylesheets must finish before anything is painted, so their CSSOM is built; classic scripts pause the parser while defer and module scripts wait until parsing is done. With DOM and CSSOM the engine computes styles, runs layout to get geometry, paints into layers and composites them on the GPU; the first paint is the first frame that completes that pipeline and LCP is when the largest element lands. After that, every interaction goes through the same style-layout-paint-composite steps in a 16 ms frame budget, which is why a long task or a layout-thrashing handler shows up directly as a bad INP."
"Reading geometry, offsetHeight, getBoundingClientRect, getComputedStyle, after a style change forces the browser to run layout synchronously so the value is current; alternating writes and reads in a loop forces that layout again and again, which is thrashing. The fix is to batch: read everything first, then write, ideally inside requestAnimationFrame, so one layout serves all reads. ResizeObserver replaces measuring in resize handlers, and often the measurement disappears entirely when CSS can express the constraint with grid or aspect-ratio. In the Performance panel it appears as a run of purple layout blocks with 'forced reflow' warnings."
Nothing yet. Notes filed here show up on this page and the section overview.
Browser, HTTP & networking
The platform underneath every framework — how browsers turn bytes into pixels, how HTTP and caching work, what fetch, CORS and cookies actually do, what browser storage and service workers offer, and which web APIs a senior should know by name.
HTTP and caching
HTTP/1.1 to HTTP/3, methods and status codes worth knowing, and caching as the biggest lever you control — Cache-Control directives, validators and 304s, Vary, CDN behaviour, immutable hashed assets, stale-while-revalidate, resource hints, early hints and speculation rules.