[{"data":1,"prerenderedAt":1949},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance":1941},{"id":4,"title":5,"body":6,"description":1924,"extension":1925,"meta":1926,"navigation":544,"path":1936,"seo":1937,"stem":1939,"__hash__":1940},"content\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Findex.md","Font Loading & Text Rendering Performance",{"type":7,"value":8,"toc":1909},"minimark",[9,14,24,50,187,190,195,198,212,222,232,236,296,300,307,388,394,398,401,700,703,707,713,844,847,929,946,950,956,1077,1092,1096,1109,1384,1399,1403,1410,1487,1502,1506,1509,1548,1554,1558,1575,1585,1595,1611,1622,1626,1629,1778,1792,1795,1799,1811,1825,1834,1849,1853,1891,1896,1899,1902,1905],[10,11,13],"h1",{"id":12},"font-loading-text-rendering-performance-painting-text-inside-the-lcp-budget","Font Loading & Text Rendering Performance: Painting Text Inside the LCP Budget",[15,16,17,18,23],"p",{},"This guide extends the rendering-pipeline work in ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement"," into the one subsystem that can fail two metrics with a single request: the web font.",[15,25,26,27,31,32,36,37,41,42,45,46,49],{},"A font is unusual among page resources because it is discovered late, blocks paint by default, and changes text geometry when it finally arrives. On a text-first page the ",[19,28,30],{"href":29},"\u002Fcore-web-vitals-measurement\u002Fmeasuring-lcp-with-chrome-devtools\u002F","Largest Contentful Paint"," element is usually a heading or paragraph, so the font sits directly on the LCP critical path — the browser will not paint that text until the face resolves or its block period expires. Then, at the moment it does resolve, every glyph is remeasured, and if the fallback occupied a different amount of space the surrounding content jumps, feeding ",[19,33,35],{"href":34},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002F","Cumulative Layout Shift",". One under-specified ",[38,39,40],"code",{},"@font-face"," rule can push LCP past ",[38,43,44],{},"2.5s"," and CLS past ",[38,47,48],{},"0.1"," at the same time.",[51,52,59,60,67,68,67,72,67,76,67,85,67,92,67,99,67,105,67,109,67,113,67,116,67,119,67,128,67,133,67,139,67,144,67,150,67,155,67,159,67,163,67,167,67,172,67,178,67,182,67],"svg",{"xmlns":53,"viewBox":54,"width":55,"role":56,"ariaLabel":57,"style":58},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 300","100%","img","Timeline showing the font block period, swap period and failure period against the 2.5 second LCP budget, with invisible text during the block period and a layout shift at the swap","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[61,62],"rect",{"className":63,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],"svg-canvas","0","#ffffff"," ",[69,70,71],"title",{},"The font display timeline against the LCP budget",[73,74,75],"desc",{},"A time axis from zero to four seconds showing a 100 millisecond block period where text is invisible, a swap period where the fallback is painted, and the swap moment where the layout shifts, all compared against the 2.5 second LCP threshold.",[61,77],{"x":78,"y":78,"width":79,"height":80,"rx":81,"fill":82,"stroke":83,"style":84},"1","758","298","10","none","currentColor","stroke-opacity:0.18",[86,87,91],"text",{"x":88,"y":89,"fill":83,"style":90},"24","36","font-size:17px;font-weight:700","One font request, two failing metrics",[93,94],"line",{"x1":95,"y1":96,"x2":97,"y2":96,"stroke":83,"style":98},"60","230","720","stroke-opacity:0.45",[86,100,104],{"x":95,"y":101,"fill":83,"style":102,"textAnchor":103},"252","font-size:11px;","middle","0s",[86,106,108],{"x":107,"y":101,"fill":83,"style":102,"textAnchor":103},"225","1s",[86,110,112],{"x":111,"y":101,"fill":83,"style":102,"textAnchor":103},"390","2s",[86,114,44],{"x":115,"y":101,"fill":83,"style":102,"textAnchor":103},"472",[86,117,118],{"x":97,"y":101,"fill":83,"style":102,"textAnchor":103},"4s",[93,120],{"x1":115,"y1":121,"x2":115,"y2":122,"stroke":123,"strokeDashArray":124,"style":127},"70","238","#b8860b",[125,126],"5","4","stroke-opacity:0.9",[86,129,132],{"x":115,"y":130,"fill":123,"style":131,"textAnchor":103},"62","font-size:11.5px;font-weight:700;","LCP budget 2.5s",[61,134],{"x":95,"y":135,"width":136,"height":137,"rx":126,"fill":83,"stroke":83,"style":138},"88","66","34","fill-opacity:0.14;stroke-opacity:0.4",[86,140,143],{"x":141,"y":142,"fill":83,"style":102,"textAnchor":103},"93","110","block",[61,145],{"x":146,"y":135,"width":147,"height":137,"rx":126,"fill":148,"stroke":148,"style":149},"126","264","#0466c8","fill-opacity:0.16;stroke-opacity:0.5",[86,151,154],{"x":152,"y":142,"fill":83,"style":153,"textAnchor":103},"258","font-size:11.5px;","swap period — fallback painted, text is readable",[61,156],{"x":111,"y":135,"width":157,"height":137,"rx":126,"fill":83,"stroke":83,"style":158},"330","fill-opacity:0.05;stroke-opacity:0.28",[86,160,162],{"x":161,"y":142,"fill":83,"style":153,"textAnchor":103},"555","failure period — fallback is permanent",[86,164,166],{"x":88,"y":142,"fill":83,"style":165},"font-size:11.5px;font-weight:600","display",[168,169],"circle",{"cx":146,"cy":170,"r":171,"fill":148},"160","6",[86,173,177],{"x":174,"y":175,"fill":83,"style":176},"140","164","font-size:11.5px","Text first paints here — this timestamp is your text LCP",[168,179],{"cx":157,"cy":180,"r":171,"fill":181,"stroke":123},"196","#ffc300",[86,183,186],{"x":184,"y":185,"fill":83,"style":176},"344","200","Font arrives, glyphs remeasured — every line below moves (CLS)",[15,188,189],{},"Both failures are avoidable, and neither is fixed by \"loading the font faster\" alone. The work is to shorten discovery, choose a display strategy that matches the content's role, and make the fallback occupy the same box as the real face so the swap is invisible.",[191,192,194],"h2",{"id":193},"the-metric-damage-a-font-actually-does","The Metric Damage a Font Actually Does",[15,196,197],{},"Quantify the two failure modes before touching a config, because they have different owners and different fixes.",[15,199,200,204,205,208,209,211],{},[201,202,203],"strong",{},"Text LCP delay."," With the default ",[38,206,207],{},"font-display: auto"," (which Chromium treats as ",[38,210,143],{},"), the browser hides text for up to three seconds while waiting for the face. On a fast connection this is invisible; on a p75 mobile connection the font is often still in flight at second two, and because the LCP candidate is that hidden text, LCP is not recorded until the swap. The damage lands entirely in the render-delay phase — the phase that a bigger CDN or a smaller image cannot help.",[15,213,214,217,218,221],{},[201,215,216],{},"Swap shift."," When the fallback and the web font have different per-glyph advance widths and line heights, the paragraph reflows on arrival. A single hero paragraph re-wrapping from three lines to four moves everything beneath it by one line height. A shift of a 900px-wide, 24px-tall block on a 1280×720 viewport contributes roughly ",[38,219,220],{},"0.02"," to CLS; three such blocks on one page is a fifth of the entire budget spent on typography.",[15,223,224,225,228,229,231],{},"The two interact in a way that traps teams: switching to ",[38,226,227],{},"font-display: swap"," removes the invisible-text delay and improves LCP, then immediately introduces the shift that ",[38,230,143],{}," was accidentally suppressing. Fixing one without the other just moves the failure between metrics.",[191,233,235],{"id":234},"prerequisites","Prerequisites",[237,238,239,259,267,274],"ul",{},[240,241,242,243,246,247,250,251,254,255,258],"li",{},"Chrome 124+ (or any current Chromium) with the ",[201,244,245],{},"Network",", ",[201,248,249],{},"Performance"," and ",[201,252,253],{},"Rendering"," panels, plus the ",[201,256,257],{},"Layout Shift Regions"," overlay for visual confirmation of swap-induced movement.",[240,260,261,262,266],{},"The font files themselves, self-hosted. Third-party font CSS adds a connection and a redirect before the face is even discovered — see ",[19,263,265],{"href":264},"\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Fpreloading-google-fonts-without-blocking-render\u002F","preloading Google Fonts without blocking render"," for the exact ordering when you cannot self-host.",[240,268,269,270,273],{},"WOFF2 builds for every face you actually ship. If your pipeline still emits WOFF or TTF alongside, confirm the ",[38,271,272],{},"src"," list puts WOFF2 first — every current browser takes it.",[240,275,276,277,280,281,246,284,287,288,291,292,295],{},"A metrics-capable subsetting tool (",[38,278,279],{},"fonttools","\u002F",[38,282,283],{},"pyftsubset",[38,285,286],{},"glyphhanger",", or a build-time equivalent) and a way to read the fallback's metrics — ",[38,289,290],{},"capsize"," or the browser's own ",[38,293,294],{},"document.fonts"," measurements both work.",[191,297,299],{"id":298},"_1-inventory-what-you-are-actually-loading","1. Inventory What You Are Actually Loading",[15,301,302,303,306],{},"Start from what the browser fetched, not from what the stylesheet declares. Load the page with an empty cache, filter the Network panel to ",[38,304,305],{},"Font",", and record three numbers per file: transferred bytes, start time, and whether the request was initiated by a stylesheet or a preload.",[308,309,314],"pre",{"className":310,"code":311,"language":312,"meta":313,"style":313},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Every font the page requested, in request order, with size and timing.\nnpx lighthouse https:\u002F\u002Fexample.com --output=json --quiet \\\n  | jq -r '.audits[\"network-requests\"].details.items[]\n           | select(.resourceType==\"Font\")\n           | \"\\(.startTime|floor)ms  \\(.transferSize)B  \\(.url)\"'\n# trade-off: Lighthouse's simulated throttle inflates absolute timings. Use this\n# for the ORDER and the byte counts; take real timestamps from field data.\n","bash","",[38,315,316,324,349,364,370,376,382],{"__ignoreMap":313},[317,318,320],"span",{"class":93,"line":319},1,[317,321,323],{"class":322},"sjfSM","# Every font the page requested, in request order, with size and timing.\n",[317,325,327,331,335,338,342,345],{"class":93,"line":326},2,[317,328,330],{"class":329},"sQw3B","npx",[317,332,334],{"class":333},"sZ8jY"," lighthouse",[317,336,337],{"class":333}," https:\u002F\u002Fexample.com",[317,339,341],{"class":340},"sPXB4"," --output=json",[317,343,344],{"class":340}," --quiet",[317,346,348],{"class":347},"sPARh"," \\\n",[317,350,352,355,358,361],{"class":93,"line":351},3,[317,353,354],{"class":347},"  |",[317,356,357],{"class":329}," jq",[317,359,360],{"class":340}," -r",[317,362,363],{"class":333}," '.audits[\"network-requests\"].details.items[]\n",[317,365,367],{"class":93,"line":366},4,[317,368,369],{"class":333},"           | select(.resourceType==\"Font\")\n",[317,371,373],{"class":93,"line":372},5,[317,374,375],{"class":333},"           | \"\\(.startTime|floor)ms  \\(.transferSize)B  \\(.url)\"'\n",[317,377,379],{"class":93,"line":378},6,[317,380,381],{"class":322},"# trade-off: Lighthouse's simulated throttle inflates absolute timings. Use this\n",[317,383,385],{"class":93,"line":384},7,[317,386,387],{"class":322},"# for the ORDER and the byte counts; take real timestamps from field data.\n",[15,389,390,391,393],{},"Two findings are common and both are quick wins. The first is faces nobody uses: an italic and a 300-weight shipped because the type foundry's snippet included them, downloaded on every visit for text that never renders in them. The second is duplicate formats, where a legacy WOFF is fetched alongside the WOFF2 because the ",[38,392,272],{}," list is ordered oldest-first.",[191,395,397],{"id":396},"_2-capture-a-baseline-for-both-metrics","2. Capture a Baseline for Both Metrics",[15,399,400],{},"Attribute the current damage before changing anything, so you can prove the fix later. Capture LCP with its phases and CLS with its sources in one pass:",[308,402,406],{"className":403,"code":404,"language":405,"meta":313,"style":313},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Text-LCP render delay and the shifts that follow the swap, in one observer pair.\nnew PerformanceObserver((list) => {\n  const e = list.getEntries().at(-1);\n  console.log('LCP', Math.round(e.startTime), e.element?.tagName,\n    'renderDelay≈', Math.round(e.startTime - (e.loadTime || e.startTime)));\n}).observe({ type: 'largest-contentful-paint', buffered: true });\n\nnew PerformanceObserver((list) => {\n  for (const s of list.getEntries()) {\n    if (s.hadRecentInput) continue;              \u002F\u002F ignore user-initiated movement\n    console.log('shift', s.value.toFixed(4),\n      s.sources?.map((x) => x.node?.nodeName).join(','));\n  }\n}).observe({ type: 'layout-shift', buffered: true });\n\u002F\u002F trade-off: console logging is a debugging aid, not instrumentation. In\n\u002F\u002F production, batch these into a single beacon on visibilitychange instead.\n","javascript",[38,407,408,413,438,472,494,517,540,546,563,588,606,632,664,670,688,694],{"__ignoreMap":313},[317,409,410],{"class":93,"line":319},[317,411,412],{"class":322},"\u002F\u002F Text-LCP render delay and the shifts that follow the swap, in one observer pair.\n",[317,414,415,418,422,426,429,432,435],{"class":93,"line":326},[317,416,417],{"class":347},"new",[317,419,421],{"class":420},"smZ65"," PerformanceObserver",[317,423,425],{"class":424},"saISM","((",[317,427,428],{"class":329},"list",[317,430,431],{"class":424},") ",[317,433,434],{"class":347},"=>",[317,436,437],{"class":424}," {\n",[317,439,440,443,446,449,452,455,458,461,464,467,469],{"class":93,"line":351},[317,441,442],{"class":347},"  const",[317,444,445],{"class":340}," e",[317,447,448],{"class":347}," =",[317,450,451],{"class":424}," list.",[317,453,454],{"class":420},"getEntries",[317,456,457],{"class":424},"().",[317,459,460],{"class":420},"at",[317,462,463],{"class":424},"(",[317,465,466],{"class":347},"-",[317,468,78],{"class":340},[317,470,471],{"class":424},");\n",[317,473,474,477,480,482,485,488,491],{"class":93,"line":366},[317,475,476],{"class":424},"  console.",[317,478,479],{"class":420},"log",[317,481,463],{"class":424},[317,483,484],{"class":333},"'LCP'",[317,486,487],{"class":424},", Math.",[317,489,490],{"class":420},"round",[317,492,493],{"class":424},"(e.startTime), e.element?.tagName,\n",[317,495,496,499,501,503,506,508,511,514],{"class":93,"line":372},[317,497,498],{"class":333},"    'renderDelay≈'",[317,500,487],{"class":424},[317,502,490],{"class":420},[317,504,505],{"class":424},"(e.startTime ",[317,507,466],{"class":347},[317,509,510],{"class":424}," (e.loadTime ",[317,512,513],{"class":347},"||",[317,515,516],{"class":424}," e.startTime)));\n",[317,518,519,522,525,528,531,534,537],{"class":93,"line":378},[317,520,521],{"class":424},"}).",[317,523,524],{"class":420},"observe",[317,526,527],{"class":424},"({ type: ",[317,529,530],{"class":333},"'largest-contentful-paint'",[317,532,533],{"class":424},", buffered: ",[317,535,536],{"class":340},"true",[317,538,539],{"class":424}," });\n",[317,541,542],{"class":93,"line":384},[317,543,545],{"emptyLinePlaceholder":544},true,"\n",[317,547,549,551,553,555,557,559,561],{"class":93,"line":548},8,[317,550,417],{"class":347},[317,552,421],{"class":420},[317,554,425],{"class":424},[317,556,428],{"class":329},[317,558,431],{"class":424},[317,560,434],{"class":347},[317,562,437],{"class":424},[317,564,566,569,572,575,578,581,583,585],{"class":93,"line":565},9,[317,567,568],{"class":347},"  for",[317,570,571],{"class":424}," (",[317,573,574],{"class":347},"const",[317,576,577],{"class":340}," s",[317,579,580],{"class":347}," of",[317,582,451],{"class":424},[317,584,454],{"class":420},[317,586,587],{"class":424},"()) {\n",[317,589,591,594,597,600,603],{"class":93,"line":590},10,[317,592,593],{"class":347},"    if",[317,595,596],{"class":424}," (s.hadRecentInput) ",[317,598,599],{"class":347},"continue",[317,601,602],{"class":424},";              ",[317,604,605],{"class":322},"\u002F\u002F ignore user-initiated movement\n",[317,607,609,612,614,616,619,622,625,627,629],{"class":93,"line":608},11,[317,610,611],{"class":424},"    console.",[317,613,479],{"class":420},[317,615,463],{"class":424},[317,617,618],{"class":333},"'shift'",[317,620,621],{"class":424},", s.value.",[317,623,624],{"class":420},"toFixed",[317,626,463],{"class":424},[317,628,126],{"class":340},[317,630,631],{"class":424},"),\n",[317,633,635,638,641,643,646,648,650,653,656,658,661],{"class":93,"line":634},12,[317,636,637],{"class":424},"      s.sources?.",[317,639,640],{"class":420},"map",[317,642,425],{"class":424},[317,644,645],{"class":329},"x",[317,647,431],{"class":424},[317,649,434],{"class":347},[317,651,652],{"class":424}," x.node?.nodeName).",[317,654,655],{"class":420},"join",[317,657,463],{"class":424},[317,659,660],{"class":333},"','",[317,662,663],{"class":424},"));\n",[317,665,667],{"class":93,"line":666},13,[317,668,669],{"class":424},"  }\n",[317,671,673,675,677,679,682,684,686],{"class":93,"line":672},14,[317,674,521],{"class":424},[317,676,524],{"class":420},[317,678,527],{"class":424},[317,680,681],{"class":333},"'layout-shift'",[317,683,533],{"class":424},[317,685,536],{"class":340},[317,687,539],{"class":424},[317,689,691],{"class":93,"line":690},15,[317,692,693],{"class":322},"\u002F\u002F trade-off: console logging is a debugging aid, not instrumentation. In\n",[317,695,697],{"class":93,"line":696},16,[317,698,699],{"class":322},"\u002F\u002F production, batch these into a single beacon on visibilitychange instead.\n",[15,701,702],{},"If the LCP element is a text node and its render delay is large while its resource load time is zero, the font is the cause: nothing was loading for that element, the browser was simply refusing to paint it.",[191,704,706],{"id":705},"_3-shorten-discovery-before-optimizing-bytes","3. Shorten Discovery Before Optimizing Bytes",[15,708,709,710,712],{},"The font request usually starts late for a structural reason, not a bandwidth one. The browser must download the HTML, then the CSS, then parse the ",[38,711,40],{}," rule, then match a rendered element to that face before it will fetch anything. That is three round trips of latency before a single font byte moves.",[51,714,59,717,67,720,67,723,67,726,67,728,67,731,67,735,67,742,67,747,67,753,67,758,67,761,67,765,67,769,67,772,67,776,67,780,67,784,67,787,67,792,67,796,67,801,67,805,67,808,67,810,67,813,67,816,67,819,67,821,67,823,67,826,67,829,67,834,67,840,67],{"xmlns":53,"viewBox":715,"width":55,"role":56,"ariaLabel":716,"style":58},"0 0 760 260","Two request chains compared: the default chain from HTML to CSS to font-face match to font fetch, versus a preload chain where the font starts in parallel with the CSS",[61,718],{"className":719,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],[69,721,722],{},"Font discovery: default chain versus preload",[73,724,725],{},"The default path serialises HTML, stylesheet, font-face matching and the font fetch, while a preload link starts the font download in parallel with the stylesheet.",[61,727],{"x":78,"y":78,"width":79,"height":152,"rx":81,"fill":82,"stroke":83,"style":84},[86,729,730],{"x":88,"y":137,"fill":83,"style":90},"Where the font request starts",[86,732,734],{"x":88,"y":136,"fill":83,"style":733},"font-size:12px;font-weight:700","Default",[61,736],{"x":737,"y":738,"width":739,"height":740,"rx":125,"fill":83,"stroke":83,"style":741},"96","52","130","30","fill-opacity:0.05;stroke-opacity:0.32",[86,743,746],{"x":744,"y":745,"fill":83,"style":153,"textAnchor":103},"161","72","HTML",[93,748],{"x1":749,"y1":750,"x2":751,"y2":750,"stroke":83,"style":752},"226","67","248","stroke-opacity:0.4",[754,755],"polygon",{"points":756,"fill":83,"style":757},"256,67 246,62 246,72","fill-opacity:0.55",[61,759],{"x":760,"y":738,"width":739,"height":740,"rx":125,"fill":83,"stroke":83,"style":741},"256",[86,762,764],{"x":763,"y":745,"fill":83,"style":153,"textAnchor":103},"321","stylesheet",[93,766],{"x1":767,"y1":750,"x2":768,"y2":750,"stroke":83,"style":752},"386","408",[754,770],{"points":771,"fill":83,"style":757},"416,67 406,62 406,72",[61,773],{"x":774,"y":738,"width":775,"height":740,"rx":125,"fill":83,"stroke":83,"style":741},"416","150",[86,777,779],{"x":778,"y":745,"fill":83,"style":153,"textAnchor":103},"491","@font-face match",[93,781],{"x1":782,"y1":750,"x2":783,"y2":750,"stroke":83,"style":752},"566","588",[754,785],{"points":786,"fill":83,"style":757},"596,67 586,62 586,72",[61,788],{"x":789,"y":738,"width":790,"height":740,"rx":125,"fill":181,"stroke":123,"style":791},"596","124","fill-opacity:0.22",[86,793,795],{"x":794,"y":745,"fill":83,"style":131,"textAnchor":103},"658","font fetch",[86,797,800],{"x":789,"y":798,"fill":83,"style":799},"102","font-size:11px;fill-opacity:0.75","starts after 3 hops",[93,802],{"x1":88,"y1":146,"x2":803,"y2":146,"stroke":83,"style":804},"736","stroke-opacity:0.2",[86,806,807],{"x":88,"y":175,"fill":83,"style":733},"Preload",[61,809],{"x":737,"y":775,"width":739,"height":740,"rx":125,"fill":83,"stroke":83,"style":741},[86,811,746],{"x":744,"y":812,"fill":83,"style":153,"textAnchor":103},"170",[93,814],{"x1":749,"y1":815,"x2":751,"y2":815,"stroke":83,"style":752},"165",[754,817],{"points":818,"fill":83,"style":757},"256,165 246,160 246,170",[61,820],{"x":760,"y":775,"width":739,"height":740,"rx":125,"fill":83,"stroke":83,"style":741},[86,822,764],{"x":763,"y":812,"fill":83,"style":153,"textAnchor":103},[61,824],{"x":760,"y":825,"width":739,"height":740,"rx":125,"fill":181,"stroke":123,"style":791},"190",[86,827,795],{"x":763,"y":828,"fill":83,"style":131,"textAnchor":103},"210",[86,830,833],{"x":831,"y":828,"fill":83,"style":832},"400","font-size:11px;fill-opacity:0.8","parallel with the CSS — one hop, not three",[93,835],{"x1":836,"y1":837,"x2":836,"y2":838,"stroke":123,"style":839},"240","182","205","stroke-opacity:0.8",[754,841],{"points":842,"fill":123,"style":843},"248,205 238,200 238,210","fill-opacity:0.85",[15,845,846],{},"Preloading collapses that chain, but only for faces you are certain will render. Each preload competes for bandwidth with the HTML-blocking CSS, so preloading five faces to save one is a net loss. The rule that holds up in production: preload exactly the faces used by above-the-fold text — usually one body weight and one heading weight — and let everything else be discovered normally.",[308,848,852],{"className":849,"code":850,"language":851,"meta":313,"style":313},"language-html shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u003C!-- In \u003Chead>, BEFORE the stylesheet that declares the face. -->\n\u003Clink rel=\"preload\" href=\"\u002Ffonts\u002Finter-var-latin.woff2\" as=\"font\"\n      type=\"font\u002Fwoff2\" crossorigin>\n\u003C!-- trade-off: a preload is a promise that this exact file WILL be used. If the\n     face is swapped, resubsetted, or only used below the fold, the preload is\n     wasted bandwidth on the critical path and Chrome logs an unused-preload\n     warning. Re-check preloads whenever the font build changes. -->\n","html",[38,853,854,859,893,909,914,919,924],{"__ignoreMap":313},[317,855,856],{"class":93,"line":319},[317,857,858],{"class":322},"\u003C!-- In \u003Chead>, BEFORE the stylesheet that declares the face. -->\n",[317,860,861,864,868,871,874,877,880,882,885,888,890],{"class":93,"line":326},[317,862,863],{"class":424},"\u003C",[317,865,867],{"class":866},"sZBmE","link",[317,869,870],{"class":340}," rel",[317,872,873],{"class":424},"=",[317,875,876],{"class":333},"\"preload\"",[317,878,879],{"class":340}," href",[317,881,873],{"class":424},[317,883,884],{"class":333},"\"\u002Ffonts\u002Finter-var-latin.woff2\"",[317,886,887],{"class":340}," as",[317,889,873],{"class":424},[317,891,892],{"class":333},"\"font\"\n",[317,894,895,898,900,903,906],{"class":93,"line":351},[317,896,897],{"class":340},"      type",[317,899,873],{"class":424},[317,901,902],{"class":333},"\"font\u002Fwoff2\"",[317,904,905],{"class":340}," crossorigin",[317,907,908],{"class":424},">\n",[317,910,911],{"class":93,"line":366},[317,912,913],{"class":322},"\u003C!-- trade-off: a preload is a promise that this exact file WILL be used. If the\n",[317,915,916],{"class":93,"line":372},[317,917,918],{"class":322},"     face is swapped, resubsetted, or only used below the fold, the preload is\n",[317,920,921],{"class":93,"line":378},[317,922,923],{"class":322},"     wasted bandwidth on the critical path and Chrome logs an unused-preload\n",[317,925,926],{"class":93,"line":384},[317,927,928],{"class":322},"     warning. Re-check preloads whenever the font build changes. -->\n",[15,930,931,932,935,936,940,941,945],{},"The ",[38,933,934],{},"crossorigin"," attribute is mandatory even for same-origin fonts: fonts are fetched in anonymous CORS mode, so a preload without it fetches a ",[937,938,939],"em",{},"second",", separate copy rather than priming the one the CSS will use. This class of mistake — a preload that does not match the eventual request — is common enough to have its own walkthrough in ",[19,942,944],{"href":943},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Ffixing-unused-preload-warnings-in-the-console\u002F","fixing unused-preload warnings",".",[191,947,949],{"id":948},"_4-choose-a-display-strategy-per-content-role","4. Choose a Display Strategy Per Content Role",[15,951,952,955],{},[38,953,954],{},"font-display"," is not a site-wide setting. It is a per-face decision about what should happen during the window where the font is not yet available, and the right answer depends on whether the text is the LCP candidate and whether the face is decorative.",[957,958,959,981],"table",{},[960,961,962],"thead",{},[963,964,965,969,972,975,978],"tr",{},[966,967,968],"th",{},"Value",[966,970,971],{},"Block period",[966,973,974],{},"Swap period",[966,976,977],{},"Best for",[966,979,980],{},"Metric effect",[982,983,984,1007,1025,1043,1060],"tbody",{},[963,985,986,995,998,1001,1004],{},[987,988,989,992,993],"td",{},[38,990,991],{},"auto"," \u002F ",[38,994,143],{},[987,996,997],{},"~3s",[987,999,1000],{},"infinite",[987,1002,1003],{},"Nothing on a content page",[987,1005,1006],{},"Text LCP waits for the font",[963,1008,1009,1014,1017,1019,1022],{},[987,1010,1011],{},[38,1012,1013],{},"swap",[987,1015,1016],{},"~0ms",[987,1018,1000],{},[987,1020,1021],{},"Body copy, headings",[987,1023,1024],{},"Fast text paint; shift unless metrics match",[963,1026,1027,1032,1035,1037,1040],{},[987,1028,1029],{},[38,1030,1031],{},"fallback",[987,1033,1034],{},"~100ms",[987,1036,997],{},[987,1038,1039],{},"Long-form article body",[987,1041,1042],{},"Small block risk, bounded shift window",[963,1044,1045,1050,1052,1054,1057],{},[987,1046,1047],{},[38,1048,1049],{},"optional",[987,1051,1034],{},[987,1053,82],{},[987,1055,1056],{},"Fonts you can live without",[987,1058,1059],{},"No shift after first paint; font may be skipped",[963,1061,1062,1067,1069,1071,1074],{},[987,1063,1064,1066],{},[38,1065,1013],{}," + overrides",[987,1068,1016],{},[987,1070,1000],{},[987,1072,1073],{},"Brand-critical headings",[987,1075,1076],{},"Fast paint AND no shift",[15,1078,1079,1080,1082,1083,250,1085,1087,1088,945],{},"The last row is the target state for most sites: ",[38,1081,1013],{}," gives you the fast first paint, and metric overrides remove the reason the swap moves anything. The trade-off between ",[38,1084,1013],{},[38,1086,1049],{}," — which one wins depends on whether you can tune the fallback — is worked through in ",[19,1089,1091],{"href":1090},"\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Ffont-display-swap-vs-optional-for-lcp\u002F","font-display: swap vs optional for LCP",[191,1093,1095],{"id":1094},"_5-make-the-fallback-occupy-the-same-box","5. Make the Fallback Occupy the Same Box",[15,1097,1098,1099,246,1102,250,1105,1108],{},"The swap only shifts layout because two fonts disagree about geometry. ",[38,1100,1101],{},"size-adjust",[38,1103,1104],{},"ascent-override",[38,1106,1107],{},"descent-override"," let you declare a local fallback face whose metrics match the web font, so the fallback renders in exactly the space the real font will later fill.",[308,1110,1114],{"className":1111,"code":1112,"language":1113,"meta":313,"style":313},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F* The real face: fast paint, no invisible-text period. *\u002F\n@font-face {\n  font-family: \"Inter\";\n  src: url(\"\u002Ffonts\u002Finter-var-latin.woff2\") format(\"woff2-variations\");\n  font-weight: 100 900;\n  font-display: swap;\n  unicode-range: U+0000-00FF, U+2000-206F;   \u002F* latin + punctuation only *\u002F\n}\n\n\u002F* The metric-matched fallback the browser paints during the swap period. *\u002F\n@font-face {\n  font-family: \"Inter Fallback\";\n  src: local(\"Arial\");\n  size-adjust: 107.4%;        \u002F* per-glyph advance width, measured not guessed *\u002F\n  ascent-override: 90.2%;\n  descent-override: 22.5%;\n  line-gap-override: 0%;\n}\n\nbody { font-family: \"Inter\", \"Inter Fallback\", sans-serif; }\n\u002F* trade-off: these percentages are specific to ONE fallback on ONE platform.\n   Arial and Roboto differ, so a single override set is a compromise across\n   your device mix — measure against your two most common fallbacks and accept\n   a sub-pixel residual on the rest. *\u002F\n","css",[38,1115,1116,1121,1127,1141,1167,1182,1193,1214,1219,1223,1228,1234,1245,1261,1280,1294,1308,1322,1327,1332,1360,1366,1372,1378],{"__ignoreMap":313},[317,1117,1118],{"class":93,"line":319},[317,1119,1120],{"class":322},"\u002F* The real face: fast paint, no invisible-text period. *\u002F\n",[317,1122,1123,1125],{"class":93,"line":326},[317,1124,40],{"class":347},[317,1126,437],{"class":424},[317,1128,1129,1132,1135,1138],{"class":93,"line":351},[317,1130,1131],{"class":340},"  font-family",[317,1133,1134],{"class":424},": ",[317,1136,1137],{"class":333},"\"Inter\"",[317,1139,1140],{"class":424},";\n",[317,1142,1143,1146,1148,1151,1153,1155,1157,1160,1162,1165],{"class":93,"line":366},[317,1144,1145],{"class":340},"  src",[317,1147,1134],{"class":424},[317,1149,1150],{"class":340},"url",[317,1152,463],{"class":424},[317,1154,884],{"class":333},[317,1156,431],{"class":424},[317,1158,1159],{"class":340},"format",[317,1161,463],{"class":424},[317,1163,1164],{"class":333},"\"woff2-variations\"",[317,1166,471],{"class":424},[317,1168,1169,1172,1174,1177,1180],{"class":93,"line":372},[317,1170,1171],{"class":340},"  font-weight",[317,1173,1134],{"class":424},[317,1175,1176],{"class":340},"100",[317,1178,1179],{"class":340}," 900",[317,1181,1140],{"class":424},[317,1183,1184,1187,1189,1191],{"class":93,"line":378},[317,1185,1186],{"class":340},"  font-display",[317,1188,1134],{"class":424},[317,1190,1013],{"class":340},[317,1192,1140],{"class":424},[317,1194,1195,1198,1200,1203,1205,1208,1211],{"class":93,"line":384},[317,1196,1197],{"class":340},"  unicode-range",[317,1199,1134],{"class":424},[317,1201,1202],{"class":340},"U+0000-00FF",[317,1204,246],{"class":424},[317,1206,1207],{"class":340},"U+2000-206F",[317,1209,1210],{"class":424},";   ",[317,1212,1213],{"class":322},"\u002F* latin + punctuation only *\u002F\n",[317,1215,1216],{"class":93,"line":548},[317,1217,1218],{"class":424},"}\n",[317,1220,1221],{"class":93,"line":565},[317,1222,545],{"emptyLinePlaceholder":544},[317,1224,1225],{"class":93,"line":590},[317,1226,1227],{"class":322},"\u002F* The metric-matched fallback the browser paints during the swap period. *\u002F\n",[317,1229,1230,1232],{"class":93,"line":608},[317,1231,40],{"class":347},[317,1233,437],{"class":424},[317,1235,1236,1238,1240,1243],{"class":93,"line":634},[317,1237,1131],{"class":340},[317,1239,1134],{"class":424},[317,1241,1242],{"class":333},"\"Inter Fallback\"",[317,1244,1140],{"class":424},[317,1246,1247,1249,1251,1254,1256,1259],{"class":93,"line":666},[317,1248,1145],{"class":340},[317,1250,1134],{"class":424},[317,1252,1253],{"class":340},"local",[317,1255,463],{"class":424},[317,1257,1258],{"class":333},"\"Arial\"",[317,1260,471],{"class":424},[317,1262,1263,1266,1268,1271,1274,1277],{"class":93,"line":672},[317,1264,1265],{"class":340},"  size-adjust",[317,1267,1134],{"class":424},[317,1269,1270],{"class":340},"107.4",[317,1272,1273],{"class":347},"%",[317,1275,1276],{"class":424},";        ",[317,1278,1279],{"class":322},"\u002F* per-glyph advance width, measured not guessed *\u002F\n",[317,1281,1282,1285,1287,1290,1292],{"class":93,"line":690},[317,1283,1284],{"class":340},"  ascent-override",[317,1286,1134],{"class":424},[317,1288,1289],{"class":340},"90.2",[317,1291,1273],{"class":347},[317,1293,1140],{"class":424},[317,1295,1296,1299,1301,1304,1306],{"class":93,"line":696},[317,1297,1298],{"class":340},"  descent-override",[317,1300,1134],{"class":424},[317,1302,1303],{"class":340},"22.5",[317,1305,1273],{"class":347},[317,1307,1140],{"class":424},[317,1309,1311,1314,1316,1318,1320],{"class":93,"line":1310},17,[317,1312,1313],{"class":340},"  line-gap-override",[317,1315,1134],{"class":424},[317,1317,65],{"class":340},[317,1319,1273],{"class":347},[317,1321,1140],{"class":424},[317,1323,1325],{"class":93,"line":1324},18,[317,1326,1218],{"class":424},[317,1328,1330],{"class":93,"line":1329},19,[317,1331,545],{"emptyLinePlaceholder":544},[317,1333,1335,1338,1341,1344,1346,1348,1350,1352,1354,1357],{"class":93,"line":1334},20,[317,1336,1337],{"class":866},"body",[317,1339,1340],{"class":424}," { ",[317,1342,1343],{"class":340},"font-family",[317,1345,1134],{"class":424},[317,1347,1137],{"class":333},[317,1349,246],{"class":424},[317,1351,1242],{"class":333},[317,1353,246],{"class":424},[317,1355,1356],{"class":340},"sans-serif",[317,1358,1359],{"class":424},"; }\n",[317,1361,1363],{"class":93,"line":1362},21,[317,1364,1365],{"class":322},"\u002F* trade-off: these percentages are specific to ONE fallback on ONE platform.\n",[317,1367,1369],{"class":93,"line":1368},22,[317,1370,1371],{"class":322},"   Arial and Roboto differ, so a single override set is a compromise across\n",[317,1373,1375],{"class":93,"line":1374},23,[317,1376,1377],{"class":322},"   your device mix — measure against your two most common fallbacks and accept\n",[317,1379,1381],{"class":93,"line":1380},24,[317,1382,1383],{"class":322},"   a sub-pixel residual on the rest. *\u002F\n",[15,1385,1386,1387,1390,1391,1394,1395,945],{},"Measured overrides typically take the swap contribution to CLS from ",[38,1388,1389],{},"0.05–0.15"," down to under ",[38,1392,1393],{},"0.005",", which is inside measurement noise. The step-by-step measurement procedure — including how to derive each percentage rather than copying someone else's — is in ",[19,1396,1398],{"href":1397},"\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Ffixing-cls-from-web-font-swap\u002F","fixing CLS from web font swap",[191,1400,1402],{"id":1401},"_6-cut-bytes-with-subsetting-and-unicode-range","6. Cut Bytes with Subsetting and unicode-range",[15,1404,1405,1406,1409],{},"A full variable font with every script is often 200–400KB; the Latin subset of the same face is 15–30KB. Subsetting is the largest single byte saving available in the font pipeline, and it is safe as long as the ",[38,1407,1408],{},"unicode-range"," descriptor tells the browser exactly which codepoints each file covers.",[308,1411,1413],{"className":310,"code":1412,"language":312,"meta":313,"style":313},"# Latin + punctuation subset of a variable font, keeping the weight axis intact.\npyftsubset inter-var.ttf \\\n  --output-file=inter-var-latin.woff2 --flavor=woff2 \\\n  --layout-features='kern,liga,calt' \\\n  --unicodes=\"U+0000-00FF,U+2000-206F,U+2212\" \\\n  --no-hinting --desubroutinize\n# trade-off: an aggressive subset breaks the moment content introduces a\n# codepoint you dropped (a user name, a currency symbol, a quoted phrase in\n# another script). Ship a second range-scoped file for those instead of\n# widening the primary subset for everyone.\n",[38,1414,1415,1420,1429,1439,1449,1459,1467,1472,1477,1482],{"__ignoreMap":313},[317,1416,1417],{"class":93,"line":319},[317,1418,1419],{"class":322},"# Latin + punctuation subset of a variable font, keeping the weight axis intact.\n",[317,1421,1422,1424,1427],{"class":93,"line":326},[317,1423,283],{"class":329},[317,1425,1426],{"class":333}," inter-var.ttf",[317,1428,348],{"class":347},[317,1430,1431,1434,1437],{"class":93,"line":351},[317,1432,1433],{"class":340},"  --output-file=inter-var-latin.woff2",[317,1435,1436],{"class":340}," --flavor=woff2",[317,1438,348],{"class":347},[317,1440,1441,1444,1447],{"class":93,"line":366},[317,1442,1443],{"class":340},"  --layout-features=",[317,1445,1446],{"class":333},"'kern,liga,calt'",[317,1448,348],{"class":347},[317,1450,1451,1454,1457],{"class":93,"line":372},[317,1452,1453],{"class":340},"  --unicodes=",[317,1455,1456],{"class":333},"\"U+0000-00FF,U+2000-206F,U+2212\"",[317,1458,348],{"class":347},[317,1460,1461,1464],{"class":93,"line":378},[317,1462,1463],{"class":340},"  --no-hinting",[317,1465,1466],{"class":340}," --desubroutinize\n",[317,1468,1469],{"class":93,"line":384},[317,1470,1471],{"class":322},"# trade-off: an aggressive subset breaks the moment content introduces a\n",[317,1473,1474],{"class":93,"line":548},[317,1475,1476],{"class":322},"# codepoint you dropped (a user name, a currency symbol, a quoted phrase in\n",[317,1478,1479],{"class":93,"line":565},[317,1480,1481],{"class":322},"# another script). Ship a second range-scoped file for those instead of\n",[317,1483,1484],{"class":93,"line":590},[317,1485,1486],{"class":322},"# widening the primary subset for everyone.\n",[15,1488,1489,1490,1492,1493,1497,1498,1501],{},"Because ",[38,1491,1408],{}," is evaluated per file, a page that never renders Cyrillic never downloads the Cyrillic file at all — the byte saving is conditional and automatic. Serve every one of these files with a long-lived immutable cache policy as described in ",[19,1494,1496],{"href":1495},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002F","HTTP Cache-Control headers explained","; font files are content-hashed and never change, which makes them the ideal ",[38,1499,1500],{},"immutable"," candidate.",[191,1503,1505],{"id":1504},"deconstructing-the-text-paint-timeline","Deconstructing the Text-Paint Timeline",[15,1507,1508],{},"Break the delay into phases and you can tell which fix applies, instead of trying all of them.",[237,1510,1511,1521,1531,1542],{},[240,1512,1513,1516,1517,1520],{},[201,1514,1515],{},"Discovery delay"," — HTML byte zero to font request start. Owned by the request chain: preload, Early Hints, and self-hosting shorten it. Target under ",[38,1518,1519],{},"300ms"," on a p75 mobile profile.",[240,1522,1523,1526,1527,1530],{},[201,1524,1525],{},"Transfer time"," — request start to response complete. Owned by bytes and connection reuse: subsetting and WOFF2 shorten it. A 20KB Latin subset transfers in well under ",[38,1528,1529],{},"200ms"," on a warm connection.",[240,1532,1533,1535,1536,1538,1539,1541],{},[201,1534,971],{}," — the window where text is deliberately invisible. Owned entirely by ",[38,1537,954],{},". This is the phase that most directly inflates text LCP render delay, and ",[38,1540,1013],{}," reduces it to approximately zero.",[240,1543,1544,1547],{},[201,1545,1546],{},"Swap moment"," — the reflow when the face applies. Owned by metric overrides. It contributes nothing to LCP but everything to font-driven CLS.",[15,1549,1550,1551,1553],{},"The clean separation matters: if your baseline shows a short discovery delay and a large render delay, no amount of preloading will help — the block period is the problem and the fix is ",[38,1552,954],{},". If discovery is late but the block period is already zero, the reverse is true.",[191,1555,1557],{"id":1556},"advanced-diagnostics-and-edge-cases","Advanced Diagnostics and Edge Cases",[15,1559,1560,1563,1564,1567,1568,1571,1572,1574],{},[201,1561,1562],{},"Icon fonts are the worst case of every category."," They are usually ",[38,1565,1566],{},"font-display: block"," by inheritance of the default, they render into fixed-size boxes so the fallback shows a blank or a ",[38,1569,1570],{},".notdef"," glyph, and they are frequently loaded from a third party. Replacing an icon font with inline SVG removes a render-blocking request, a shift source, and a connection at once. Where migration is not feasible, ",[38,1573,1566],{}," is genuinely the right choice for icons — a blank icon box for 100ms beats a flash of unrelated glyphs.",[15,1576,1577,1580,1581,1584],{},[201,1578,1579],{},"Variable fonts change the arithmetic."," A single variable file replacing four static weights usually wins on total bytes and always wins on request count, but the single file is larger, so the ",[937,1582,1583],{},"first"," weight paints later than a static equivalent would. On pages that use exactly one weight above the fold, a static subset for that weight plus a lazily-loaded variable file for the rest paints fastest.",[15,1586,1587,1590,1591,1594],{},[201,1588,1589],{},"Framework hydration can re-trigger the swap."," In an SSR framework, a component that re-renders text with a different class after hydration can produce a second remeasure well after load. Since layout shifts within 500ms of an interaction are excluded but hydration is not an interaction, that shift counts in full. Detect it by watching for ",[38,1592,1593],{},"layout-shift"," entries with a timestamp after the hydration mark, and fix it by ensuring the server and client render identical typography classes.",[15,1596,1597,1603,1604,1606,1607,1610],{},[201,1598,1599,1602],{},[38,1600,1601],{},"document.fonts.ready"," is not a paint signal."," It resolves when the font loading process finishes, which on a page using ",[38,1605,1049],{}," may be ",[937,1608,1609],{},"after"," the browser has already decided not to use the face. Treat it as a diagnostic hook, never as a gate for showing content — gating content on it reintroduces invisible text under a different name.",[15,1612,1613,67,1619,1621],{},[201,1614,1615,1616,1618],{},"Preload plus ",[38,1617,1049],{}," is a deliberate combination.",[38,1620,1049],{}," gives the browser a ~100ms window to have the font available; without a preload, the font almost never arrives in time on a cold cache, so first-time visitors always get the fallback. Preloading it makes the window winnable while keeping the guarantee that a late font is never swapped in.",[191,1623,1625],{"id":1624},"validation-and-budgeting-in-ci","Validation and Budgeting in CI",[15,1627,1628],{},"Prove the fix with two assertions — one for each metric the font can damage — and one audit that stops a regression at the source.",[308,1630,1634],{"className":1631,"code":1632,"language":1633,"meta":313,"style":313},"language-json shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","{\n  \"ci\": {\n    \"collect\": { \"numberOfRuns\": 3 },\n    \"assert\": {\n      \"assertions\": {\n        \"font-display\": [\"error\", { \"minScore\": 1 }],\n        \"cumulative-layout-shift\": [\"error\", { \"maxNumericValue\": 0.05 }],\n        \"largest-contentful-paint\": [\"error\", { \"maxNumericValue\": 2500 }],\n        \"uses-text-compression\": [\"off\"]\n      }\n    }\n  }\n}\n","json",[38,1635,1636,1641,1649,1668,1675,1682,1706,1727,1747,1760,1765,1770,1774],{"__ignoreMap":313},[317,1637,1638],{"class":93,"line":319},[317,1639,1640],{"class":424},"{\n",[317,1642,1643,1646],{"class":93,"line":326},[317,1644,1645],{"class":866},"  \"ci\"",[317,1647,1648],{"class":424},": {\n",[317,1650,1651,1654,1657,1660,1662,1665],{"class":93,"line":351},[317,1652,1653],{"class":866},"    \"collect\"",[317,1655,1656],{"class":424},": { ",[317,1658,1659],{"class":866},"\"numberOfRuns\"",[317,1661,1134],{"class":424},[317,1663,1664],{"class":340},"3",[317,1666,1667],{"class":424}," },\n",[317,1669,1670,1673],{"class":93,"line":366},[317,1671,1672],{"class":866},"    \"assert\"",[317,1674,1648],{"class":424},[317,1676,1677,1680],{"class":93,"line":372},[317,1678,1679],{"class":866},"      \"assertions\"",[317,1681,1648],{"class":424},[317,1683,1684,1687,1690,1693,1696,1699,1701,1703],{"class":93,"line":378},[317,1685,1686],{"class":866},"        \"font-display\"",[317,1688,1689],{"class":424},": [",[317,1691,1692],{"class":333},"\"error\"",[317,1694,1695],{"class":424},", { ",[317,1697,1698],{"class":866},"\"minScore\"",[317,1700,1134],{"class":424},[317,1702,78],{"class":340},[317,1704,1705],{"class":424}," }],\n",[317,1707,1708,1711,1713,1715,1717,1720,1722,1725],{"class":93,"line":384},[317,1709,1710],{"class":866},"        \"cumulative-layout-shift\"",[317,1712,1689],{"class":424},[317,1714,1692],{"class":333},[317,1716,1695],{"class":424},[317,1718,1719],{"class":866},"\"maxNumericValue\"",[317,1721,1134],{"class":424},[317,1723,1724],{"class":340},"0.05",[317,1726,1705],{"class":424},[317,1728,1729,1732,1734,1736,1738,1740,1742,1745],{"class":93,"line":548},[317,1730,1731],{"class":866},"        \"largest-contentful-paint\"",[317,1733,1689],{"class":424},[317,1735,1692],{"class":333},[317,1737,1695],{"class":424},[317,1739,1719],{"class":866},[317,1741,1134],{"class":424},[317,1743,1744],{"class":340},"2500",[317,1746,1705],{"class":424},[317,1748,1749,1752,1754,1757],{"class":93,"line":565},[317,1750,1751],{"class":866},"        \"uses-text-compression\"",[317,1753,1689],{"class":424},[317,1755,1756],{"class":333},"\"off\"",[317,1758,1759],{"class":424},"]\n",[317,1761,1762],{"class":93,"line":590},[317,1763,1764],{"class":424},"      }\n",[317,1766,1767],{"class":93,"line":608},[317,1768,1769],{"class":424},"    }\n",[317,1771,1772],{"class":93,"line":634},[317,1773,669],{"class":424},[317,1775,1776],{"class":93,"line":666},[317,1777,1218],{"class":424},[15,1779,931,1780,1782,1783,1785,1786,1788,1789,1791],{},[38,1781,954],{}," audit fails any face left on the default block behaviour, which catches the most common regression: a new ",[38,1784,40],{}," added by a design update without a display descriptor. The CLS assertion is set tighter than the ",[38,1787,48],{}," Core Web Vitals boundary on purpose — typography should not consume half the budget, and a ",[38,1790,1724],{}," ceiling leaves room for the shifts you cannot control.",[15,1793,1794],{},"For field confirmation, segment your RUM data by whether the font was served from cache. First-time visitors experience the entire discovery-plus-transfer cost; repeat visitors experience none of it. A p75 that looks fine in aggregate can hide a first-visit LCP well over the budget when the cache-hit population is large.",[191,1796,1798],{"id":1797},"faq","FAQ",[1800,1801,1804,1808],"details",{"className":1802},[1803],"faq-item",[1805,1806,1807],"summary",{},"Should I preload every font the page uses?",[15,1809,1810],{},"No. A preload moves a request to the front of the queue, and the queue is finite — preloading five faces delays the stylesheet and the LCP image to save a font that was not on the critical path. Preload only the faces that render above the fold, which is typically one body weight and one heading weight. Everything else should be discovered normally so it competes for bandwidth after the critical resources are in flight.",[1800,1812,1814,1817],{"className":1813},[1803],[1805,1815,1816],{},"Why did my CLS get worse after I switched to font-display: swap?",[15,1818,1489,1819,1821,1822,1824],{},[38,1820,1013],{}," did not create the shift, it revealed it. With the default block behaviour the browser hid the text until the font arrived, so there was never a fallback frame to shift away from — at the cost of a much later text paint. Once you paint the fallback first, any geometry difference between the two faces becomes a visible reflow. The fix is not to revert but to add ",[38,1823,1101],{}," and the ascent\u002Fdescent overrides so the fallback occupies the same box.",[1800,1826,1828,1831],{"className":1827},[1803],[1805,1829,1830],{},"Is self-hosting always faster than a font CDN?",[15,1832,1833],{},"For the first render, almost always: self-hosting removes a DNS lookup, a TLS handshake and (for the common Google Fonts pattern) a CSS request that must complete before the font file is even known. Cross-site caching no longer offsets this, because browsers partition their caches per top-level site, so a visitor never reuses another site's copy of the font. A font CDN still wins on convenience and on serving per-browser formats automatically, which matters if your build cannot subset.",[1800,1835,1837,1840],{"className":1836},[1803],[1805,1838,1839],{},"How do I know which fallback to tune the metric overrides against?",[15,1841,1842,1843,1845,1846,945],{},"Tune against the fallback your users actually get, which is decided by the last entry in your ",[38,1844,1343],{}," stack and the platform. Read the device breakdown in your field data: a mostly-Android audience falls back to Roboto, a mostly-iOS one to a system face, and a desktop-heavy one to Arial or Helvetica. If two fallbacks are close in share, tune to the larger one and verify that the residual shift for the other stays under ",[38,1847,1848],{},"0.01",[191,1850,1852],{"id":1851},"related","Related",[237,1854,1855,1861,1867,1873,1878,1884],{},[240,1856,1857,1860],{},[19,1858,1859],{"href":34},"Reducing Cumulative Layout Shift"," — the full CLS diagnostic workflow that font swap shifts feed into.",[240,1862,1863,1866],{},[19,1864,1865],{"href":29},"Measuring LCP with Chrome DevTools"," — isolate whether your LCP element is text blocked by a font or an image blocked by bytes.",[240,1868,1869,1872],{},[19,1870,1871],{"href":1397},"Fixing CLS from web font swap"," — measure and derive the exact override percentages for your fallback.",[240,1874,1875,1877],{},[19,1876,1091],{"href":1090}," — pick the display strategy that matches the face's role.",[240,1879,1880,1883],{},[19,1881,1882],{"href":264},"Preloading Google Fonts without blocking render"," — the correct hint ordering when the faces stay on a third-party origin.",[240,1885,1886,1890],{},[19,1887,1889],{"href":1888},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002F","Resource Hints & Early Hints"," — start the font fetch before the HTML response even finishes.",[1892,1893,1895],"script",{"type":1894},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Optimize web font loading for LCP and CLS\",\n  \"description\": \"A workflow for auditing font requests, shortening discovery, choosing a font-display strategy, matching fallback metrics, and subsetting so text paints inside the LCP budget without shifting layout.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Inventory loaded fonts\", \"text\": \"List every font the page actually requests with its bytes, start time and initiator.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F#1-inventory-what-you-are-actually-loading\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Capture a two-metric baseline\", \"text\": \"Record LCP phases and layout-shift sources before changing any font configuration.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F#2-capture-a-baseline-for-both-metrics\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Shorten discovery\", \"text\": \"Preload the above-the-fold faces with crossorigin so the font fetch starts in parallel with the stylesheet.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F#3-shorten-discovery-before-optimizing-bytes\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Choose font-display per face\", \"text\": \"Match the display strategy to the role of the text: swap for body and headings, optional for non-essential faces, block only for icons.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F#4-choose-a-display-strategy-per-content-role\" },\n    { \"@type\": \"HowToStep\", \"position\": 5, \"name\": \"Match fallback metrics\", \"text\": \"Declare a local fallback face with size-adjust and ascent\u002Fdescent overrides so the swap does not move the layout.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F#5-make-the-fallback-occupy-the-same-box\" },\n    { \"@type\": \"HowToStep\", \"position\": 6, \"name\": \"Subset and budget\", \"text\": \"Cut bytes with unicode-range subsets, then lock the result in with font-display, CLS and LCP assertions in CI.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F#6-cut-bytes-with-subsetting-and-unicode-range\" }\n  ]\n}\n",[1892,1897,1898],{"type":1894},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Font Loading & Text Rendering Performance: Painting Text Inside the LCP Budget\",\n  \"description\": \"How to stop web fonts from delaying text LCP and shifting layout: discovery, font-display strategy, metric overrides, subsetting and CI budgets.\",\n  \"datePublished\": \"2026-08-01\",\n  \"dateModified\": \"2026-08-01\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F\" }\n}\n",[1892,1900,1901],{"type":1894},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    { \"@type\": \"Question\", \"name\": \"Should I preload every font the page uses?\", \"acceptedAnswer\": { \"@type\": \"Answer\", \"text\": \"No. Preload only the faces that render above the fold, typically one body weight and one heading weight. Preloading everything delays the stylesheet and the LCP image to prioritise fonts that were not on the critical path.\" } },\n    { \"@type\": \"Question\", \"name\": \"Why did my CLS get worse after I switched to font-display: swap?\", \"acceptedAnswer\": { \"@type\": \"Answer\", \"text\": \"Swap revealed a shift that the default block behaviour was hiding by not painting text at all. Add size-adjust and ascent\u002Fdescent overrides to a local fallback face so the fallback occupies the same box as the web font.\" } },\n    { \"@type\": \"Question\", \"name\": \"Is self-hosting always faster than a font CDN?\", \"acceptedAnswer\": { \"@type\": \"Answer\", \"text\": \"For first render, almost always, because it removes a DNS lookup, a TLS handshake and a blocking CSS request. Browser cache partitioning means a visitor never reuses another site's copy of the font, so cross-site caching no longer offsets the extra connection.\" } },\n    { \"@type\": \"Question\", \"name\": \"How do I know which fallback to tune the metric overrides against?\", \"acceptedAnswer\": { \"@type\": \"Answer\", \"text\": \"Tune against the fallback your field data says users get: Roboto for Android-heavy audiences, a system face on iOS, Arial or Helvetica on desktop. Tune to the largest share and verify the residual shift for the second fallback stays under 0.01.\" } }\n  ]\n}\n",[1892,1903,1904],{"type":1894},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Core Web Vitals & Measurement\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Font Loading & Text Rendering Performance\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F\" }\n  ]\n}\n",[1906,1907,1908],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":313,"searchDepth":326,"depth":326,"links":1910},[1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923],{"id":193,"depth":326,"text":194},{"id":234,"depth":326,"text":235},{"id":298,"depth":326,"text":299},{"id":396,"depth":326,"text":397},{"id":705,"depth":326,"text":706},{"id":948,"depth":326,"text":949},{"id":1094,"depth":326,"text":1095},{"id":1401,"depth":326,"text":1402},{"id":1504,"depth":326,"text":1505},{"id":1556,"depth":326,"text":1557},{"id":1624,"depth":326,"text":1625},{"id":1797,"depth":326,"text":1798},{"id":1851,"depth":326,"text":1852},"Engineer the font pipeline so text paints inside the LCP budget and the swap never moves the page: discovery, font-display, metric overrides and subsetting.","md",{"slug":1927,"type":1928,"breadcrumb":1929,"datePublished":1935,"dateModified":1935},"font-loading-and-text-rendering-performance","topic",[1930,1932,1933],{"name":1931,"url":280},"Home",{"name":22,"url":21},{"name":5,"url":1934},"\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002F","2026-08-01","\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance",{"title":5,"description":1938},"Stop web fonts from delaying text LCP and shifting layout: block\u002Fswap periods, preload ordering, font-display choices, metric overrides, subsetting, and CI budgets.","core-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Findex","rQPEZE0b2RAX-_Pb7xLmW2KuGuyuKPxjoz_u22Q8Z9k",[1942,1945],{"title":22,"path":1943,"stem":1944},"\u002Fcore-web-vitals-measurement","core-web-vitals-measurement\u002Findex",{"title":1946,"path":1947,"stem":1948},"Fixing CLS from Web Font Swap","\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Ffixing-cls-from-web-font-swap","core-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Ffixing-cls-from-web-font-swap\u002Findex",1785622406322]