Keentune

Web Performance curriculum

19 chapters
·
141 concepts
·
free
Everything the adaptive question bank can teach and test in Web Performance, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Web Performance guide
A free 16-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. Why speed matters and what a good metric is
LCP measures loading, INP responsiveness, CLS visual stability
the questions are "is it happening, useful, usable, delightful", not "how many bytes shipped"
each vital has three bands, not one cut-off, and "needs improvement" is not a pass
vitals are assessed at the 75th percentile of page loads, so a healthy average can still fail
a URL only "passes Core Web Vitals" when LCP, INP and CLS are all in the good band
TTFB, FCP and TBT diagnose a Core Web Vital; they are not themselves Core Web Vitals
the population you optimize for is a mid-range Android on a slow link, not your laptop on wifi
faster pages measurably raise conversion and cut bounce; that is the argument for the work
B. Largest Contentful Paint — what it measures
good is ≤2.5s at p75; above 4.0s is poor
only <img>, <image> in SVG, video poster, CSS background-image, and block-level text qualify
the candidate is re-elected as content paints, and freezes at the first scroll, tap or keypress
the reported size is the smaller of on-screen and intrinsic size, and clipped or overflowing area does not count
FCP is the first pixel of any content; LCP is the paint of the largest one
a text LCP has no resource to fetch, so its time is font and render delay, not bytes
a client-side route change is not a new page load, so it produces no new LCP
C. Diagnosing and fixing LCP
LCP = TTFB + resource load delay + resource load duration + element render delay
a healthy split is roughly 40% TTFB, under 10% load delay, 40% load duration, under 10% render delay
load delay is usually the hero being injected by JS instead of sitting in the initial HTML
loading="lazy" on the LCP image delays it until after layout, costing hundreds of ms
fetchpriority="high" promotes the hero out of the browser's default low image priority
<link rel=preload as=image imagesrcset imagesizes> preloads the right candidate of a responsive hero
the image can be in memory and still not painted while CSS or a blocking script runs
D. Cumulative Layout Shift — what it measures
good is ≤0.1 at p75; above 0.25 is poor, and the score is unitless
one shift scores impact fraction × distance fraction, both relative to the viewport
CLS is the worst 5-second burst of shifts (gaps under 1s), not the sum of every shift
a shift within 500ms of a user input is expected and excluded via hadRecentInput
moving an element with transform changes no layout position, so it scores zero
an element appearing is not a shift; the already-visible content it pushes down is
CLS keeps accumulating after load, so a shift on scroll or on an ad refresh still counts
E. Preventing layout shift
width and height on <img> let the browser reserve the box before the bytes arrive
CSS aspect-ratio reserves the box for fluid media whose pixel size is unknown
size a slot from the historical p-something of that slot, and never collapse it when the ad fails
a cookie or promo bar inserted above existing content shifts the whole page; overlay or pre-reserve it
swapping from fallback to webfont reflows text whenever the two fonts' metrics differ
animating top/width shifts layout every frame; transform does not
give a lazily-loaded or hydrated component a min-height so its arrival changes nothing
F. Interaction to Next Paint
good is ≤200ms at p75; above 500ms is poor
clicks, taps and key presses count; scrolling and hovering do not
input delay, then event processing, then presentation delay before the next frame
INP reports the page's worst interaction, discarding one outlier per 50 interactions
the clock stops at the painted frame, so a fast handler that queues heavy rendering still scores badly
one tap fires pointerdown, pointerup and click; the interaction's latency is the longest of the group
INP replaced FID in March 2024 because FID only measured the first interaction's input delay
a page nobody interacts with reports no INP at all, which is not the same as a good INP
G. Long tasks and the main thread
any uninterrupted main-thread task over 50ms is a long task, and input cannot be handled during it
TBT sums each long task's time beyond 50ms; it is the lab proxy for INP
a setTimeout(…, 0) breaks work into separate tasks so pending input can run between them
scheduler.yield() yields but resumes ahead of other queued tasks, unlike setTimeout
moving non-visual work until after the next frame improves INP without doing less work
debounce runs once after the input settles; throttle runs at a fixed rate during it
a worker runs off the main thread but cannot touch the DOM, and postMessage copies data
a growing heap lengthens garbage-collection pauses, and a GC pause is a main-thread stall that surfaces as an INP spike
H. The cost of JavaScript
JS is the most expensive byte type because it is downloaded, parsed, compiled and run
the network cost tracks the compressed size, but parse and execute track the uncompressed size
dynamic import() defers a route's or a widget's code until it is actually needed
dead-code elimination needs static ES modules and honest sideEffects metadata
compiling to an ancient target ships polyfills and slower output to modern browsers
hydration re-runs the component tree on the client, which is main-thread work no server render removes
one giant bundle invalidates the whole cache on any change; too many chunks cost requests
I. Script loading and third parties
a plain <script> stops HTML parsing until it downloads and executes
defer keeps document order and runs before DOMContentLoaded; async runs as soon as it lands, in any order
type="module" is deferred by default, so adding defer to it changes nothing
a secondary parser fetches subresources ahead of the parser, and JS-injected tags are invisible to it
a vendor tag runs on your main thread at your priority with no size limit you control
replace a heavy embed with a static placeholder that loads the real thing on interaction
J. The network path to the first byte
good TTFB is ≤0.8s; it is a diagnostic for LCP, not a Core Web Vital itself
redirect time, DNS, connection, TLS, request, and the server's own think time
every redirect costs a full round trip, and a chain multiplies it before any HTML arrives
DNS plus TCP plus TLS is roughly three round trips before the first request is even sent
HTTP/2 interleaves many streams on one connection, removing the six-connection limit and domain sharding
HTTP/3 over QUIC ends TCP head-of-line blocking and can resume a connection in 0-RTT
Brotli beats gzip on text; re-compressing JPEG, WebP or a zip gains nothing
K. Resource hints and priority
opens DNS, TCP and TLS to an origin early; each one costs, so use it for a handful of critical origins
resolves DNS only, so it is far cheaper than preconnect and safe to fan out wider
rel=preload without a correct as gets the wrong priority and can download the file twice
a preload nothing consumes wastes bandwidth and logs a console warning
rel=prefetch is a lowest-priority fetch for a page the user will probably visit next
preloads and pre-parses an ES module plus its dependency graph, which plain preload does not
high/low/auto nudge a resource within its own class rather than jumping it above CSS
CSS and blocking scripts load highest, images start low and are promoted once layout proves they are visible
L. Images and video
AVIF is usually smallest, WebP is the safe default, and JPEG/PNG are the fallbacks
w descriptors plus sizes let the browser pick by layout width and device pixel ratio
a sizes value that does not match the CSS layout makes the browser pick the wrong candidate
<picture> with type gives format fallback, and with media gives real art direction
loading="lazy" only helps off-screen images, and hurts anything above the fold
a huge image costs main-thread decode time even after it downloads; decoding="async" moves it off the critical path
images and video are the largest share of transferred bytes on the median page, which is why image work is the highest-leverage saving
a base64 data URI removes a request and adds about a third to the bytes, in a file that must parse before render, and it cannot be cached separately
preload="none" plus a poster keeps an unplayed video from downloading megabytes
M. Web fonts
block, swap, fallback and optional differ only in how long text is invisible and how long a swap stays allowed
FOIT hides text until the font loads; FOUT shows a fallback and swaps, trading a blank screen for a shift
a font is only requested after CSS parses and a matching element exists, which is why the critical one is preloaded
unicode-range splits a family so a script's glyph file is only fetched when those characters appear
WOFF2 is the format to ship, and one variable font can replace a stack of weight files
size-adjust, ascent-override and friends make the fallback occupy the same space, removing the swap shift
a font CDN costs an extra connection, and cache partitioning killed the cross-site reuse that justified it
N. CSS and the critical rendering path
the browser needs both the DOM and the CSSOM before it can build a render tree and paint
a stylesheet in the head blocks the first paint by default, which is why one slow CSS file blanks the page
a media that does not match makes the sheet non-render-blocking, though it is still downloaded
@import inside CSS is only discovered after the parent sheet parses, adding a serial round trip
inlining above-the-fold rules and deferring the rest removes the blocking request entirely
rules that never match still cost download, parse and style recalculation on every change
style, layout and memory all scale with node count, so a huge DOM is itself a performance bug
O. Rendering: frames, layout and compositing
60Hz gives ~16.7ms per frame, and browser overhead leaves roughly 10ms for your code
JS, style, layout, paint, composite — and each step you skip is time you keep
geometry properties relayout, colors repaint, and transform/opacity only composite
reading offsetHeight after a style write forces layout immediately, before the frame needs it
alternating reads and writes in a loop forces one layout per iteration; batch reads, then writes
it promotes an element to its own layer, and too many layers cost GPU memory instead of saving time
auto skips rendering off-screen subtrees, and contain-intrinsic-size keeps the scrollbar honest
contain: layout paint promises the engine an element's layout cannot affect outside geometry and its painting cannot escape its box
scrolling runs on the compositor until a non-passive wheel or touch listener forces the main thread to be consulted on every gesture
P. HTTP caching, CDNs and the service worker
Cache-Control: max-age is freshness in seconds counted from when the response was generated
no-cache stores the response but revalidates before reuse; no-store never writes it down at all
tells the browser not to revalidate a still-fresh response even when the user reloads
a content hash in the URL plus a one-year max-age, immutable gives safe permanent caching
ETag or Last-Modified turns a re-fetch into a bodyless 304, which still costs a full round trip
s-maxage and private control the CDN separately from the browser cache
serves the stale copy instantly and refreshes it in the background, hiding the revalidation latency
the named request headers join the cache key, so Vary: User-Agent effectively disables caching
the memory cache reuses resources from RAM for the current session; the disk cache is slower but survives a restart
Cache Storage is app-controlled and ignores HTTP freshness, so a stale service worker can pin an old build
Q. Speculative loading and the back/forward cache
a JSON speculationrules script declares URLs to prefetch or prerender, with an eagerness level
prerendering executes the next page in a hidden tab, so activation is near-instant
analytics and other side effects fire early unless the page defers them until activation
back/forward can restore a frozen page from memory, JS heap intact, firing pageshow with persisted
an unload listener or Cache-Control: no-store on the document disqualifies the page
a 103 response sends Link headers so the browser preloads while the server is still building the HTML
R. Measuring: APIs, lab and field
buffered: true replays entries that fired before your observer existed
the document's own waterfall: redirect, DNS, connect, request, response, DOM and load milestones
cross-origin resource timings are zeroed unless the server sends Timing-Allow-Origin
vitals accrue for the whole page lifetime, so a RUM beacon must fire on hide, not on load
CrUX is a 28-day rolling p75 from opted-in Chrome users, and is what Search reads
a single throttled lab run, weighted mostly by TBT, LCP and CLS, with no INP in it
device, network, cache state and real interaction make lab and field disagree by design
LoAF and the attribution build name the offending element and script instead of just the number
S. Budgets and regression discipline
milestone timings, quantity budgets (bytes and requests), and rule-based scores each catch different regressions
set the number from your current p75 and the fastest competitor, not from a round figure
a budget that fails the build stops regressions; a budget in a spreadsheet documents them
synthetic runs catch bundle regressions per PR, field data confirms real users felt it
tie a speed change to the conversion or engagement metric it claims to move, or it is unproven
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Web Performance practice
Also on your phone
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC