[{"data":1,"prerenderedAt":2175},["ShallowReactive",2],{"page:\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fmeasuring-build-time-regressions-in-ci":3,"all-docs-nav":1931},{"id":4,"title":5,"body":6,"breadcrumb":1907,"dateModified":1916,"datePublished":1916,"description":1917,"extension":1918,"faq":1919,"meta":1925,"navigation":147,"path":1926,"seo":1927,"slug":12,"stem":1928,"type":1929,"__hash__":1930},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fmeasuring-build-time-regressions-in-ci\u002Findex.md","Measuring Build-Time Regressions in CI",{"type":7,"value":8,"toc":1888},"minimark",[9,13,17,26,31,44,48,51,349,359,461,465,468,1056,1059,1063,1281,1284,1384,1388,1391,1398,1404,1418,1461,1465,1468,1581,1645,1649,1652,1749,1752,1756,1794,1798,1803,1807,1812,1815,1819,1822,1826,1829,1833,1836,1840,1843,1847,1884],[10,11,5],"h1",{"id":12},"measuring-build-time-regressions-in-ci",[14,15,16],"p",{},"Build times do not degrade in one step; they degrade in a hundred small ones. A partial that scans every page, a plugin added for one feature, an image pipeline that stopped hitting its cache. Each change adds a second, nobody notices, and eighteen months later the build takes four minutes and someone proposes a migration to fix it.",[14,18,19,20,25],{},"The cure is measurement with a threshold attached: time the build step specifically, compare against a rolling baseline of comparable runs, and fail a pull request that makes it dramatically worse. This guide sets that up. It sits under ",[21,22,24],"a",{"href":23},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002F","Incremental Builds and Build Caching for SSGs",".",[27,28,30],"h2",{"id":29},"prerequisites","Prerequisites",[32,33,34,38,41],"ul",{},[35,36,37],"li",{},"A CI pipeline where the build is a distinct step you can time.",[35,39,40],{},"Somewhere to keep a history: a cache entry, a repository file, or a small metrics store.",[35,42,43],{},"A cache-restore step whose hit or miss status is available to the workflow.",[27,45,47],{"id":46},"time-the-build-not-the-job","Time the Build, Not the Job",[14,49,50],{},"Job duration is the wrong number: it includes checkout, dependency installation, tests and deploy, and it moves with runner contention. Measure the build step alone and record the context that explains it:",[52,53,58],"pre",{"className":54,"code":55,"language":56,"meta":57,"style":57},"language-bash shiki shiki-themes github-light github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\n# scripts\u002Ftimed-build.sh — emits build-metrics.json\nset -euo pipefail\nSTART=$(date +%s%3N)\nnpm run build\nEND=$(date +%s%3N)\n\nPAGES=$(find dist -name '*.html' | wc -l)\nDURATION=$(( END - START ))\n\ncat > build-metrics.json \u003C\u003CJSON\n{\n  \"duration_ms\": ${DURATION},\n  \"pages\": ${PAGES},\n  \"ms_per_page\": $(( DURATION \u002F (PAGES > 0 ? PAGES : 1) )),\n  \"cache_hit\": \"${CACHE_HIT:-unknown}\",\n  \"sha\": \"${GITHUB_SHA:-local}\",\n  \"branch\": \"${GITHUB_REF_NAME:-local}\"\n}\nJSON\ncat build-metrics.json\n","bash","",[59,60,61,70,76,90,114,126,142,149,182,204,209,227,233,244,254,280,298,314,330,336,341],"code",{"__ignoreMap":57},[62,63,66],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"sJ8bj","#!\u002Fusr\u002Fbin\u002Fenv bash\n",[62,71,73],{"class":64,"line":72},2,[62,74,75],{"class":68},"# scripts\u002Ftimed-build.sh — emits build-metrics.json\n",[62,77,79,83,86],{"class":64,"line":78},3,[62,80,82],{"class":81},"sj4cs","set",[62,84,85],{"class":81}," -euo",[62,87,89],{"class":88},"sZZnC"," pipefail\n",[62,91,93,97,101,104,108,111],{"class":64,"line":92},4,[62,94,96],{"class":95},"sVt8B","START",[62,98,100],{"class":99},"szBVR","=",[62,102,103],{"class":95},"$(",[62,105,107],{"class":106},"sScJk","date",[62,109,110],{"class":88}," +%s%3N",[62,112,113],{"class":95},")\n",[62,115,117,120,123],{"class":64,"line":116},5,[62,118,119],{"class":106},"npm",[62,121,122],{"class":88}," run",[62,124,125],{"class":88}," build\n",[62,127,129,132,134,136,138,140],{"class":64,"line":128},6,[62,130,131],{"class":95},"END",[62,133,100],{"class":99},[62,135,103],{"class":95},[62,137,107],{"class":106},[62,139,110],{"class":88},[62,141,113],{"class":95},[62,143,145],{"class":64,"line":144},7,[62,146,148],{"emptyLinePlaceholder":147},true,"\n",[62,150,152,155,157,159,162,165,168,171,174,177,180],{"class":64,"line":151},8,[62,153,154],{"class":95},"PAGES",[62,156,100],{"class":99},[62,158,103],{"class":95},[62,160,161],{"class":106},"find",[62,163,164],{"class":88}," dist",[62,166,167],{"class":81}," -name",[62,169,170],{"class":88}," '*.html'",[62,172,173],{"class":99}," |",[62,175,176],{"class":106}," wc",[62,178,179],{"class":81}," -l",[62,181,113],{"class":95},[62,183,185,188,190,193,195,198,201],{"class":64,"line":184},9,[62,186,187],{"class":95},"DURATION",[62,189,100],{"class":99},[62,191,192],{"class":95},"$(( ",[62,194,131],{"class":106},[62,196,197],{"class":88}," -",[62,199,200],{"class":88}," START",[62,202,203],{"class":95}," ))\n",[62,205,207],{"class":64,"line":206},10,[62,208,148],{"emptyLinePlaceholder":147},[62,210,212,215,218,221,224],{"class":64,"line":211},11,[62,213,214],{"class":106},"cat",[62,216,217],{"class":99}," >",[62,219,220],{"class":88}," build-metrics.json",[62,222,223],{"class":99}," \u003C\u003C",[62,225,226],{"class":88},"JSON\n",[62,228,230],{"class":64,"line":229},12,[62,231,232],{"class":88},"{\n",[62,234,236,239,241],{"class":64,"line":235},13,[62,237,238],{"class":88},"  \"duration_ms\": ${",[62,240,187],{"class":95},[62,242,243],{"class":88},"},\n",[62,245,247,250,252],{"class":64,"line":246},14,[62,248,249],{"class":88},"  \"pages\": ${",[62,251,154],{"class":95},[62,253,243],{"class":88},[62,255,257,260,262,265,268,271,274,277],{"class":64,"line":256},15,[62,258,259],{"class":88},"  \"ms_per_page\": $(( ",[62,261,187],{"class":106},[62,263,264],{"class":88}," \u002F (PAGES ",[62,266,267],{"class":99},">",[62,269,270],{"class":81}," 0",[62,272,273],{"class":88}," ? PAGES : ",[62,275,276],{"class":81},"1",[62,278,279],{"class":88},") )),\n",[62,281,283,286,289,292,295],{"class":64,"line":282},16,[62,284,285],{"class":88},"  \"cache_hit\": \"${",[62,287,288],{"class":95},"CACHE_HIT",[62,290,291],{"class":99},":-",[62,293,294],{"class":95},"unknown",[62,296,297],{"class":88},"}\",\n",[62,299,301,304,307,309,312],{"class":64,"line":300},17,[62,302,303],{"class":88},"  \"sha\": \"${",[62,305,306],{"class":95},"GITHUB_SHA",[62,308,291],{"class":99},[62,310,311],{"class":95},"local",[62,313,297],{"class":88},[62,315,317,320,323,325,327],{"class":64,"line":316},18,[62,318,319],{"class":88},"  \"branch\": \"${",[62,321,322],{"class":95},"GITHUB_REF_NAME",[62,324,291],{"class":99},[62,326,311],{"class":95},[62,328,329],{"class":88},"}\"\n",[62,331,333],{"class":64,"line":332},19,[62,334,335],{"class":88},"}\n",[62,337,339],{"class":64,"line":338},20,[62,340,226],{"class":88},[62,342,344,346],{"class":64,"line":343},21,[62,345,214],{"class":106},[62,347,348],{"class":88}," build-metrics.json\n",[14,350,351,354,355,25],{},[59,352,353],{},"ms_per_page"," is the field that survives a growing site. A corpus that doubles legitimately doubles total build time; if milliseconds per page also doubles, something in the templates got slower — the quadratic-scan pattern described in ",[21,356,358],{"href":357},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fprofiling-hugo-templates-with-template-metrics\u002F","Profiling Hugo Templates With Template Metrics",[360,361,362,457],"figure",{},[363,364,371,372,371,376,371,380,371,387],"svg",{"viewBox":365,"role":366,"ariaLabelledBy":367,"xmlns":370},"0 0 760 300","img",[368,369],"bt-signal-title","bt-signal-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[373,374,375],"title",{"id":368},"Total build time versus milliseconds per page",[377,378,379],"desc",{"id":369},"Two series over twelve months. Total build time rises steadily from 18 to 52 seconds, which looks alarming. Milliseconds per page stays flat at about 12 until month eight, then rises to 19, isolating the month where a template genuinely got slower rather than the site merely growing.",[381,382],"rect",{"x":383,"y":383,"width":384,"height":385,"fill":386},"0","760","300","#ffffff",[388,389,391,392,391,400,391,407,391,410,391,417,391,422,391,428,391,434,391,440,391,444,391,448,391,453,371],"g",{"style":390},"font-family:system-ui, sans-serif;font-size:12px","\n    ",[393,394,399],"text",{"x":395,"y":396,"fill":397,"style":398},"380","26","#1f2937","font-size:16px;font-weight:700;text-anchor:middle","Growth or regression? Only one series tells you",[64,401],{"x1":402,"y1":403,"x2":402,"y2":404,"stroke":405,"style":406},"100","52","212","#556071","stroke-width:1.5px",[64,408],{"x1":402,"y1":404,"x2":409,"y2":404,"stroke":405,"style":406},"700",[411,412],"polyline",{"points":413,"fill":414,"stroke":415,"style":416},"140,196 240,182 340,166 440,148 540,110 660,66","none","#d83b41","stroke-width:2.5px",[411,418],{"points":419,"fill":414,"stroke":420,"style":421},"140,200 240,199 340,200 440,198 540,160 660,152","#1982c4","stroke-width:2.5px;stroke-dasharray:7 5",[393,423,427],{"x":424,"y":425,"fill":415,"style":426},"560","58","font-size:11px;font-weight:700","total build · 18 s → 52 s",[393,429,433],{"x":430,"y":431,"fill":432,"style":426},"520","146","#166a9e","ms\u002Fpage · 12 → 19 at month 8",[393,435,439],{"x":436,"y":437,"fill":405,"style":438},"140","234","font-size:11px;text-anchor:middle","month 1",[393,441,443],{"x":442,"y":437,"fill":405,"style":438},"400","month 6",[393,445,447],{"x":446,"y":437,"fill":405,"style":438},"660","month 12",[393,449,452],{"x":402,"y":450,"fill":405,"style":451},"266","font-size:11px","Solid: total seconds — rises with every new page, so it always looks like a regression",[393,454,456],{"x":402,"y":455,"fill":405,"style":451},"286","Dashed: per page — flat while the site grows, and steps exactly where a template changed",[458,459,460],"figcaption",{},"Normalising by page count is what turns \"the build keeps getting slower\" into \"the build got slower in month eight, and here is the commit range\".",[27,462,464],{"id":463},"compare-against-a-rolling-baseline","Compare Against a Rolling Baseline",[14,466,467],{},"Comparing against the previous run guarantees false alarms, because runner performance varies by 10-20% between jobs. Compare against a median of recent runs on the default branch:",[52,469,473],{"className":470,"code":471,"language":472,"meta":57,"style":57},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F scripts\u002Fcheck-build-budget.mjs — compares against a rolling median\nimport { readFile, writeFile } from 'node:fs\u002Fpromises';\n\nconst current = JSON.parse(await readFile('build-metrics.json', 'utf8'));\nlet history = [];\ntry { history = JSON.parse(await readFile('.build-history.json', 'utf8')); } catch { \u002F* first run *\u002F }\n\n\u002F\u002F Compare warm builds against warm builds only\nconst comparable = history.filter((h) => h.cache_hit === current.cache_hit).slice(-20);\n\nif (comparable.length >= 5) {\n  const values = comparable.map((h) => h.ms_per_page).sort((a, b) => a - b);\n  const median = values[Math.floor(values.length \u002F 2)];\n  const delta = (current.ms_per_page - median) \u002F median;\n  const pct = (delta * 100).toFixed(1);\n\n  console.log(`ms\u002Fpage: ${current.ms_per_page} vs median ${median} (${pct > 0 ? '+' : ''}${pct}%)`);\n\n  if (delta > 0.25) {\n    console.error(`build regression: ${pct}% slower per page than the 20-run median`);\n    process.exit(1);\n  }\n  if (delta > 0.10) console.warn(`::warning::build ${pct}% slower per page than median`);\n}\n\nif (current.branch === 'main') {\n  history.push(current);\n  await writeFile('.build-history.json', JSON.stringify(history.slice(-50), null, 2));\n}\n","javascript",[59,474,475,480,497,501,543,556,602,606,611,662,666,686,737,766,789,819,823,878,882,896,916,930,936,966,971,976,991,1003,1051],{"__ignoreMap":57},[62,476,477],{"class":64,"line":65},[62,478,479],{"class":68},"\u002F\u002F scripts\u002Fcheck-build-budget.mjs — compares against a rolling median\n",[62,481,482,485,488,491,494],{"class":64,"line":72},[62,483,484],{"class":99},"import",[62,486,487],{"class":95}," { readFile, writeFile } ",[62,489,490],{"class":99},"from",[62,492,493],{"class":88}," 'node:fs\u002Fpromises'",[62,495,496],{"class":95},";\n",[62,498,499],{"class":64,"line":78},[62,500,148],{"emptyLinePlaceholder":147},[62,502,503,506,509,512,515,517,520,523,526,529,531,534,537,540],{"class":64,"line":92},[62,504,505],{"class":99},"const",[62,507,508],{"class":81}," current",[62,510,511],{"class":99}," =",[62,513,514],{"class":81}," JSON",[62,516,25],{"class":95},[62,518,519],{"class":106},"parse",[62,521,522],{"class":95},"(",[62,524,525],{"class":99},"await",[62,527,528],{"class":106}," readFile",[62,530,522],{"class":95},[62,532,533],{"class":88},"'build-metrics.json'",[62,535,536],{"class":95},", ",[62,538,539],{"class":88},"'utf8'",[62,541,542],{"class":95},"));\n",[62,544,545,548,551,553],{"class":64,"line":116},[62,546,547],{"class":99},"let",[62,549,550],{"class":95}," history ",[62,552,100],{"class":99},[62,554,555],{"class":95}," [];\n",[62,557,558,561,564,566,568,570,572,574,576,578,580,583,585,587,590,593,596,599],{"class":64,"line":128},[62,559,560],{"class":99},"try",[62,562,563],{"class":95}," { history ",[62,565,100],{"class":99},[62,567,514],{"class":81},[62,569,25],{"class":95},[62,571,519],{"class":106},[62,573,522],{"class":95},[62,575,525],{"class":99},[62,577,528],{"class":106},[62,579,522],{"class":95},[62,581,582],{"class":88},"'.build-history.json'",[62,584,536],{"class":95},[62,586,539],{"class":88},[62,588,589],{"class":95},")); } ",[62,591,592],{"class":99},"catch",[62,594,595],{"class":95}," { ",[62,597,598],{"class":68},"\u002F* first run *\u002F",[62,600,601],{"class":95}," }\n",[62,603,604],{"class":64,"line":144},[62,605,148],{"emptyLinePlaceholder":147},[62,607,608],{"class":64,"line":151},[62,609,610],{"class":68},"\u002F\u002F Compare warm builds against warm builds only\n",[62,612,613,615,618,620,623,626,629,633,636,639,642,645,648,651,653,656,659],{"class":64,"line":184},[62,614,505],{"class":99},[62,616,617],{"class":81}," comparable",[62,619,511],{"class":99},[62,621,622],{"class":95}," history.",[62,624,625],{"class":106},"filter",[62,627,628],{"class":95},"((",[62,630,632],{"class":631},"s4XuR","h",[62,634,635],{"class":95},") ",[62,637,638],{"class":99},"=>",[62,640,641],{"class":95}," h.cache_hit ",[62,643,644],{"class":99},"===",[62,646,647],{"class":95}," current.cache_hit).",[62,649,650],{"class":106},"slice",[62,652,522],{"class":95},[62,654,655],{"class":99},"-",[62,657,658],{"class":81},"20",[62,660,661],{"class":95},");\n",[62,663,664],{"class":64,"line":206},[62,665,148],{"emptyLinePlaceholder":147},[62,667,668,671,674,677,680,683],{"class":64,"line":211},[62,669,670],{"class":99},"if",[62,672,673],{"class":95}," (comparable.",[62,675,676],{"class":81},"length",[62,678,679],{"class":99}," >=",[62,681,682],{"class":81}," 5",[62,684,685],{"class":95},") {\n",[62,687,688,691,694,696,699,702,704,706,708,710,713,716,718,720,722,725,727,729,732,734],{"class":64,"line":229},[62,689,690],{"class":99},"  const",[62,692,693],{"class":81}," values",[62,695,511],{"class":99},[62,697,698],{"class":95}," comparable.",[62,700,701],{"class":106},"map",[62,703,628],{"class":95},[62,705,632],{"class":631},[62,707,635],{"class":95},[62,709,638],{"class":99},[62,711,712],{"class":95}," h.ms_per_page).",[62,714,715],{"class":106},"sort",[62,717,628],{"class":95},[62,719,21],{"class":631},[62,721,536],{"class":95},[62,723,724],{"class":631},"b",[62,726,635],{"class":95},[62,728,638],{"class":99},[62,730,731],{"class":95}," a ",[62,733,655],{"class":99},[62,735,736],{"class":95}," b);\n",[62,738,739,741,744,746,749,752,755,757,760,763],{"class":64,"line":235},[62,740,690],{"class":99},[62,742,743],{"class":81}," median",[62,745,511],{"class":99},[62,747,748],{"class":95}," values[Math.",[62,750,751],{"class":106},"floor",[62,753,754],{"class":95},"(values.",[62,756,676],{"class":81},[62,758,759],{"class":99}," \u002F",[62,761,762],{"class":81}," 2",[62,764,765],{"class":95},")];\n",[62,767,768,770,773,775,778,780,783,786],{"class":64,"line":246},[62,769,690],{"class":99},[62,771,772],{"class":81}," delta",[62,774,511],{"class":99},[62,776,777],{"class":95}," (current.ms_per_page ",[62,779,655],{"class":99},[62,781,782],{"class":95}," median) ",[62,784,785],{"class":99},"\u002F",[62,787,788],{"class":95}," median;\n",[62,790,791,793,796,798,801,804,807,810,813,815,817],{"class":64,"line":256},[62,792,690],{"class":99},[62,794,795],{"class":81}," pct",[62,797,511],{"class":99},[62,799,800],{"class":95}," (delta ",[62,802,803],{"class":99},"*",[62,805,806],{"class":81}," 100",[62,808,809],{"class":95},").",[62,811,812],{"class":106},"toFixed",[62,814,522],{"class":95},[62,816,276],{"class":81},[62,818,661],{"class":95},[62,820,821],{"class":64,"line":282},[62,822,148],{"emptyLinePlaceholder":147},[62,824,825,828,831,833,836,839,841,843,846,849,852,855,857,859,862,865,868,871,873,876],{"class":64,"line":300},[62,826,827],{"class":95},"  console.",[62,829,830],{"class":106},"log",[62,832,522],{"class":95},[62,834,835],{"class":88},"`ms\u002Fpage: ${",[62,837,838],{"class":95},"current",[62,840,25],{"class":88},[62,842,353],{"class":95},[62,844,845],{"class":88},"} vs median ${",[62,847,848],{"class":95},"median",[62,850,851],{"class":88},"} (${",[62,853,854],{"class":95},"pct",[62,856,217],{"class":99},[62,858,270],{"class":81},[62,860,861],{"class":99}," ?",[62,863,864],{"class":88}," '+'",[62,866,867],{"class":99}," :",[62,869,870],{"class":88}," ''}${",[62,872,854],{"class":95},[62,874,875],{"class":88},"}%)`",[62,877,661],{"class":95},[62,879,880],{"class":64,"line":316},[62,881,148],{"emptyLinePlaceholder":147},[62,883,884,887,889,891,894],{"class":64,"line":332},[62,885,886],{"class":99},"  if",[62,888,800],{"class":95},[62,890,267],{"class":99},[62,892,893],{"class":81}," 0.25",[62,895,685],{"class":95},[62,897,898,901,904,906,909,911,914],{"class":64,"line":338},[62,899,900],{"class":95},"    console.",[62,902,903],{"class":106},"error",[62,905,522],{"class":95},[62,907,908],{"class":88},"`build regression: ${",[62,910,854],{"class":95},[62,912,913],{"class":88},"}% slower per page than the 20-run median`",[62,915,661],{"class":95},[62,917,918,921,924,926,928],{"class":64,"line":343},[62,919,920],{"class":95},"    process.",[62,922,923],{"class":106},"exit",[62,925,522],{"class":95},[62,927,276],{"class":81},[62,929,661],{"class":95},[62,931,933],{"class":64,"line":932},22,[62,934,935],{"class":95},"  }\n",[62,937,939,941,943,945,948,951,954,956,959,961,964],{"class":64,"line":938},23,[62,940,886],{"class":99},[62,942,800],{"class":95},[62,944,267],{"class":99},[62,946,947],{"class":81}," 0.10",[62,949,950],{"class":95},") console.",[62,952,953],{"class":106},"warn",[62,955,522],{"class":95},[62,957,958],{"class":88},"`::warning::build ${",[62,960,854],{"class":95},[62,962,963],{"class":88},"}% slower per page than median`",[62,965,661],{"class":95},[62,967,969],{"class":64,"line":968},24,[62,970,335],{"class":95},[62,972,974],{"class":64,"line":973},25,[62,975,148],{"emptyLinePlaceholder":147},[62,977,979,981,984,986,989],{"class":64,"line":978},26,[62,980,670],{"class":99},[62,982,983],{"class":95}," (current.branch ",[62,985,644],{"class":99},[62,987,988],{"class":88}," 'main'",[62,990,685],{"class":95},[62,992,994,997,1000],{"class":64,"line":993},27,[62,995,996],{"class":95},"  history.",[62,998,999],{"class":106},"push",[62,1001,1002],{"class":95},"(current);\n",[62,1004,1006,1009,1012,1014,1016,1018,1021,1023,1026,1029,1031,1033,1035,1038,1041,1044,1046,1049],{"class":64,"line":1005},28,[62,1007,1008],{"class":99},"  await",[62,1010,1011],{"class":106}," writeFile",[62,1013,522],{"class":95},[62,1015,582],{"class":88},[62,1017,536],{"class":95},[62,1019,1020],{"class":81},"JSON",[62,1022,25],{"class":95},[62,1024,1025],{"class":106},"stringify",[62,1027,1028],{"class":95},"(history.",[62,1030,650],{"class":106},[62,1032,522],{"class":95},[62,1034,655],{"class":99},[62,1036,1037],{"class":81},"50",[62,1039,1040],{"class":95},"), ",[62,1042,1043],{"class":81},"null",[62,1045,536],{"class":95},[62,1047,1048],{"class":81},"2",[62,1050,542],{"class":95},[62,1052,1054],{"class":64,"line":1053},29,[62,1055,335],{"class":95},[14,1057,1058],{},"Three design choices make this usable rather than noisy. Only comparable runs enter the baseline, so a cold build after a dependency change is never measured against warm ones. The threshold is a percentage, because absolute seconds mean different things at different site sizes. And only default-branch runs update the history, so a slow experimental branch cannot poison the baseline.",[27,1060,1062],{"id":1061},"wire-it-in","Wire It In",[52,1064,1068],{"className":1065,"code":1066,"language":1067,"meta":57,"style":57},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fbuild.yml (excerpt)\n- name: Restore build cache\n  id: cache\n  uses: actions\u002Fcache@v4\n  with:\n    path: |\n      node_modules\u002F.astro\n      .cache\n    key: build-${{ hashFiles('package-lock.json') }}-${{ hashFiles('content\u002F**') }}\n    restore-keys: build-${{ hashFiles('package-lock.json') }}-\n\n- name: Build (timed)\n  env:\n    CACHE_HIT: ${{ steps.cache.outputs.cache-hit == 'true' && 'warm' || 'cold' }}\n  run: bash scripts\u002Ftimed-build.sh\n\n- name: Build budget\n  run: node scripts\u002Fcheck-build-budget.mjs\n\n- name: Persist history\n  if: github.ref == 'refs\u002Fheads\u002Fmain'\n  uses: actions\u002Fcache\u002Fsave@v4\n  with:\n    path: .build-history.json\n    key: build-history-${{ github.run_id }}\n","yaml",[59,1069,1070,1075,1090,1100,1110,1118,1128,1133,1138,1148,1158,1162,1173,1180,1190,1200,1204,1215,1224,1228,1239,1248,1257,1263,1272],{"__ignoreMap":57},[62,1071,1072],{"class":64,"line":65},[62,1073,1074],{"class":68},"# .github\u002Fworkflows\u002Fbuild.yml (excerpt)\n",[62,1076,1077,1080,1084,1087],{"class":64,"line":72},[62,1078,1079],{"class":95},"- ",[62,1081,1083],{"class":1082},"s9eBZ","name",[62,1085,1086],{"class":95},": ",[62,1088,1089],{"class":88},"Restore build cache\n",[62,1091,1092,1095,1097],{"class":64,"line":78},[62,1093,1094],{"class":1082},"  id",[62,1096,1086],{"class":95},[62,1098,1099],{"class":88},"cache\n",[62,1101,1102,1105,1107],{"class":64,"line":92},[62,1103,1104],{"class":1082},"  uses",[62,1106,1086],{"class":95},[62,1108,1109],{"class":88},"actions\u002Fcache@v4\n",[62,1111,1112,1115],{"class":64,"line":116},[62,1113,1114],{"class":1082},"  with",[62,1116,1117],{"class":95},":\n",[62,1119,1120,1123,1125],{"class":64,"line":128},[62,1121,1122],{"class":1082},"    path",[62,1124,1086],{"class":95},[62,1126,1127],{"class":99},"|\n",[62,1129,1130],{"class":64,"line":144},[62,1131,1132],{"class":88},"      node_modules\u002F.astro\n",[62,1134,1135],{"class":64,"line":151},[62,1136,1137],{"class":88},"      .cache\n",[62,1139,1140,1143,1145],{"class":64,"line":184},[62,1141,1142],{"class":1082},"    key",[62,1144,1086],{"class":95},[62,1146,1147],{"class":88},"build-${{ hashFiles('package-lock.json') }}-${{ hashFiles('content\u002F**') }}\n",[62,1149,1150,1153,1155],{"class":64,"line":206},[62,1151,1152],{"class":1082},"    restore-keys",[62,1154,1086],{"class":95},[62,1156,1157],{"class":88},"build-${{ hashFiles('package-lock.json') }}-\n",[62,1159,1160],{"class":64,"line":211},[62,1161,148],{"emptyLinePlaceholder":147},[62,1163,1164,1166,1168,1170],{"class":64,"line":229},[62,1165,1079],{"class":95},[62,1167,1083],{"class":1082},[62,1169,1086],{"class":95},[62,1171,1172],{"class":88},"Build (timed)\n",[62,1174,1175,1178],{"class":64,"line":235},[62,1176,1177],{"class":1082},"  env",[62,1179,1117],{"class":95},[62,1181,1182,1185,1187],{"class":64,"line":246},[62,1183,1184],{"class":1082},"    CACHE_HIT",[62,1186,1086],{"class":95},[62,1188,1189],{"class":88},"${{ steps.cache.outputs.cache-hit == 'true' && 'warm' || 'cold' }}\n",[62,1191,1192,1195,1197],{"class":64,"line":256},[62,1193,1194],{"class":1082},"  run",[62,1196,1086],{"class":95},[62,1198,1199],{"class":88},"bash scripts\u002Ftimed-build.sh\n",[62,1201,1202],{"class":64,"line":282},[62,1203,148],{"emptyLinePlaceholder":147},[62,1205,1206,1208,1210,1212],{"class":64,"line":300},[62,1207,1079],{"class":95},[62,1209,1083],{"class":1082},[62,1211,1086],{"class":95},[62,1213,1214],{"class":88},"Build budget\n",[62,1216,1217,1219,1221],{"class":64,"line":316},[62,1218,1194],{"class":1082},[62,1220,1086],{"class":95},[62,1222,1223],{"class":88},"node scripts\u002Fcheck-build-budget.mjs\n",[62,1225,1226],{"class":64,"line":332},[62,1227,148],{"emptyLinePlaceholder":147},[62,1229,1230,1232,1234,1236],{"class":64,"line":338},[62,1231,1079],{"class":95},[62,1233,1083],{"class":1082},[62,1235,1086],{"class":95},[62,1237,1238],{"class":88},"Persist history\n",[62,1240,1241,1243,1245],{"class":64,"line":343},[62,1242,886],{"class":1082},[62,1244,1086],{"class":95},[62,1246,1247],{"class":88},"github.ref == 'refs\u002Fheads\u002Fmain'\n",[62,1249,1250,1252,1254],{"class":64,"line":932},[62,1251,1104],{"class":1082},[62,1253,1086],{"class":95},[62,1255,1256],{"class":88},"actions\u002Fcache\u002Fsave@v4\n",[62,1258,1259,1261],{"class":64,"line":938},[62,1260,1114],{"class":1082},[62,1262,1117],{"class":95},[62,1264,1265,1267,1269],{"class":64,"line":968},[62,1266,1122],{"class":1082},[62,1268,1086],{"class":95},[62,1270,1271],{"class":88},".build-history.json\n",[62,1273,1274,1276,1278],{"class":64,"line":973},[62,1275,1142],{"class":1082},[62,1277,1086],{"class":95},[62,1279,1280],{"class":88},"build-history-${{ github.run_id }}\n",[14,1282,1283],{},"Storing the history in a cache entry rather than in the repository keeps the default branch free of commits that exist only to record a number. If you prefer the history to be reviewable, commit it — but do it from a scheduled job rather than on every merge, or the noise is worse than the problem.",[360,1285,1286,1381],{},[363,1287,371,1292,371,1295,371,1298,371,1301],{"viewBox":1288,"role":366,"ariaLabelledBy":1289,"xmlns":370},"0 0 760 290",[1290,1291],"bt-gate-title","bt-gate-desc",[373,1293,1294],{"id":1290},"How a build-time gate classifies a run",[377,1296,1297],{"id":1291},"A decision flow. If the cache state differs from the baseline, the run is not comparable and the check is skipped. Otherwise the run is compared with the median: under ten percent passes silently, ten to twenty-five percent emits a warning, and above twenty-five percent fails the pull request.",[381,1299],{"x":383,"y":383,"width":384,"height":1300,"fill":386},"290",[388,1302,391,1303,391,1307,391,1316,391,1322,391,1326,391,1331,391,1335,391,1338,391,1343,391,1347,391,1351,391,1356,391,1359,391,1362,391,1366,391,1370,391,1373,391,1377,371],{"style":390},[393,1304,1306],{"x":395,"y":1305,"fill":397,"style":398},"28","Three outcomes, one of which is silence",[381,1308],{"x":1309,"y":1310,"width":1311,"height":1312,"rx":1313,"fill":1314,"opacity":1315,"stroke":1314,"style":406},"40","60","180","56","10","#6a4c93","0.14",[393,1317,1321],{"x":1318,"y":1319,"fill":397,"style":1320},"130","84","font-size:12px;font-weight:700;text-anchor:middle","same cache state?",[393,1323,1325],{"x":1318,"y":1324,"fill":405,"style":438},"102","warm vs warm only",[381,1327],{"x":1328,"y":1310,"width":1311,"height":1312,"rx":1313,"fill":1329,"opacity":1330,"stroke":405,"style":406},"260","#d9e2ef","0.5",[393,1332,1334],{"x":1333,"y":1319,"fill":397,"style":1320},"350","no → skip",[393,1336,1337],{"x":1333,"y":1324,"fill":405,"style":438},"not comparable",[381,1339],{"x":1309,"y":431,"width":1311,"height":1312,"rx":1313,"fill":1340,"opacity":1341,"stroke":1342,"style":406},"#8ac926","0.18","#5a8a16",[393,1344,1346],{"x":1318,"y":1345,"fill":397,"style":1320},"170","\u003C 10% slower",[393,1348,1350],{"x":1318,"y":1349,"fill":1342,"style":438},"188","pass, silently",[381,1352],{"x":1328,"y":431,"width":1311,"height":1312,"rx":1313,"fill":1353,"opacity":1354,"stroke":1355,"style":406},"#ffca3a","0.3","#a97b00",[393,1357,1358],{"x":1333,"y":1345,"fill":397,"style":1320},"10–25% slower",[393,1360,1361],{"x":1333,"y":1349,"fill":1355,"style":438},"warning annotation",[381,1363],{"x":1364,"y":431,"width":1311,"height":1312,"rx":1313,"fill":1365,"opacity":1315,"stroke":415,"style":406},"480","#ff595e",[393,1367,1369],{"x":1368,"y":1345,"fill":397,"style":1320},"570","> 25% slower",[393,1371,1372],{"x":1368,"y":1349,"fill":415,"style":438},"fail the pull request",[393,1374,1376],{"x":1309,"y":1375,"fill":405,"style":451},"248","Thresholds are per page against a 20-run median on the default branch, not against the previous run",[393,1378,1380],{"x":1309,"y":1379,"fill":405,"style":451},"270","A gate that fires on runner noise gets disabled within a fortnight, which is worse than having none",[458,1382,1383],{},"The silent case is the important one. A check that comments on every pull request trains people to ignore it long before it has anything useful to say.",[27,1385,1387],{"id":1386},"read-the-regression-then-fix-it","Read the Regression, Then Fix It",[14,1389,1390],{},"When the gate fires, the number tells you there is a problem and not what it is. Three questions resolve nearly every case.",[14,1392,1393,1397],{},[1394,1395,1396],"strong",{},"Did the page count change?"," If pages grew and milliseconds per page held, there is no regression — the site is bigger. Update nothing.",[14,1399,1400,1403],{},[1394,1401,1402],{},"Did dependencies change?"," A new plugin or a major version bump is the most common single cause, and it usually shows as a step change rather than a drift. Compare the lockfile diff against the run where the number moved.",[14,1405,1406,1409,1410,1413,1414,1417],{},[1394,1407,1408],{},"Did a template change?"," This is the expensive case and the one worth profiling. Generator-specific profilers point straight at it: ",[59,1411,1412],{},"--templateMetrics"," in Hugo, the build analyser in Astro, ",[59,1415,1416],{},"DEBUG=Eleventy:Benchmark*"," in Eleventy.",[52,1419,1421],{"className":54,"code":1420,"language":56,"meta":57,"style":57},"DEBUG=Eleventy:Benchmark* npx @11ty\u002Feleventy 2>&1 | grep -E 'Benchmark.*[0-9]{4,}ms' | head -10\n",[59,1422,1423],{"__ignoreMap":57},[62,1424,1425,1428,1430,1433,1436,1439,1442,1444,1447,1450,1453,1455,1458],{"class":64,"line":65},[62,1426,1427],{"class":95},"DEBUG",[62,1429,100],{"class":99},[62,1431,1432],{"class":88},"Eleventy:Benchmark*",[62,1434,1435],{"class":106}," npx",[62,1437,1438],{"class":88}," @11ty\u002Feleventy",[62,1440,1441],{"class":99}," 2>&1",[62,1443,173],{"class":99},[62,1445,1446],{"class":106}," grep",[62,1448,1449],{"class":81}," -E",[62,1451,1452],{"class":88}," 'Benchmark.*[0-9]{4,}ms'",[62,1454,173],{"class":99},[62,1456,1457],{"class":106}," head",[62,1459,1460],{"class":81}," -10\n",[27,1462,1464],{"id":1463},"make-the-number-visible-without-nagging","Make the Number Visible Without Nagging",[14,1466,1467],{},"A gate that only speaks when something is wrong is easy to forget; one that comments on every pull request gets muted. The middle path is a job summary — visible to anyone who looks at the run, invisible to everyone else.",[52,1469,1471],{"className":54,"code":1470,"language":56,"meta":57,"style":57},"{\n  echo \"### Build\"\n  echo \"\"\n  echo \"| metric | value |\"\n  echo \"|---|---|\"\n  echo \"| duration | $(jq -r '.duration_ms\u002F1000 | floor' build-metrics.json) s |\"\n  echo \"| pages | $(jq -r .pages build-metrics.json) |\"\n  echo \"| ms\u002Fpage | $(jq -r .ms_per_page build-metrics.json) |\"\n  echo \"| cache | $(jq -r .cache_hit build-metrics.json) |\"\n} >> \"$GITHUB_STEP_SUMMARY\"\n",[59,1472,1473,1477,1485,1492,1499,1506,1522,1536,1550,1564],{"__ignoreMap":57},[62,1474,1475],{"class":64,"line":65},[62,1476,232],{"class":95},[62,1478,1479,1482],{"class":64,"line":72},[62,1480,1481],{"class":81},"  echo",[62,1483,1484],{"class":88}," \"### Build\"\n",[62,1486,1487,1489],{"class":64,"line":78},[62,1488,1481],{"class":81},[62,1490,1491],{"class":88}," \"\"\n",[62,1493,1494,1496],{"class":64,"line":92},[62,1495,1481],{"class":81},[62,1497,1498],{"class":88}," \"| metric | value |\"\n",[62,1500,1501,1503],{"class":64,"line":116},[62,1502,1481],{"class":81},[62,1504,1505],{"class":88}," \"|---|---|\"\n",[62,1507,1508,1510,1513,1516,1519],{"class":64,"line":128},[62,1509,1481],{"class":81},[62,1511,1512],{"class":88}," \"| duration | $(",[62,1514,1515],{"class":106},"jq",[62,1517,1518],{"class":81}," -r",[62,1520,1521],{"class":88}," '.duration_ms\u002F1000 | floor' build-metrics.json) s |\"\n",[62,1523,1524,1526,1529,1531,1533],{"class":64,"line":144},[62,1525,1481],{"class":81},[62,1527,1528],{"class":88}," \"| pages | $(",[62,1530,1515],{"class":106},[62,1532,1518],{"class":81},[62,1534,1535],{"class":88}," .pages build-metrics.json) |\"\n",[62,1537,1538,1540,1543,1545,1547],{"class":64,"line":151},[62,1539,1481],{"class":81},[62,1541,1542],{"class":88}," \"| ms\u002Fpage | $(",[62,1544,1515],{"class":106},[62,1546,1518],{"class":81},[62,1548,1549],{"class":88}," .ms_per_page build-metrics.json) |\"\n",[62,1551,1552,1554,1557,1559,1561],{"class":64,"line":184},[62,1553,1481],{"class":81},[62,1555,1556],{"class":88}," \"| cache | $(",[62,1558,1515],{"class":106},[62,1560,1518],{"class":81},[62,1562,1563],{"class":88}," .cache_hit build-metrics.json) |\"\n",[62,1565,1566,1569,1572,1575,1578],{"class":64,"line":206},[62,1567,1568],{"class":95},"} ",[62,1570,1571],{"class":99},">>",[62,1573,1574],{"class":88}," \"",[62,1576,1577],{"class":95},"$GITHUB_STEP_SUMMARY",[62,1579,1580],{"class":88},"\"\n",[360,1582,1583,1642],{},[363,1584,371,1589,371,1592,371,1595,371,1598],{"viewBox":1585,"role":366,"ariaLabelledBy":1586,"xmlns":370},"0 0 760 280",[1587,1588],"bt-vis-title","bt-vis-desc",[373,1590,1591],{"id":1587},"Where build metrics should appear",[377,1593,1594],{"id":1588},"Three channels ranked. A job summary is always available and never intrusive. A warning annotation appears only on a moderate regression. A failed check appears only on a large regression. A pull request comment on every run is marked as the option that gets muted.",[381,1596],{"x":383,"y":383,"width":384,"height":1597,"fill":386},"280",[388,1599,391,1600,391,1603,391,1607,391,1612,391,1616,391,1620,391,1624,391,1628,391,1631,391,1635,391,1639,371],{"style":390},[393,1601,1602],{"x":395,"y":1305,"fill":397,"style":398},"Visible when wanted, loud only when needed",[381,1604],{"x":1309,"y":1312,"width":1605,"height":403,"rx":1313,"fill":1340,"opacity":1606,"stroke":1342,"style":406},"680","0.16",[393,1608,1611],{"x":395,"y":1609,"fill":397,"style":1610},"78","font-size:13px;font-weight:700;text-anchor:middle","Job summary — every run",[393,1613,1615],{"x":395,"y":1614,"fill":405,"style":438},"98","duration, pages, ms\u002Fpage, cache state · read by anyone who opens the run",[381,1617],{"x":1309,"y":1618,"width":1605,"height":403,"rx":1313,"fill":1353,"opacity":1619,"stroke":1355,"style":406},"116","0.26",[393,1621,1623],{"x":395,"y":1622,"fill":397,"style":1610},"138","Warning annotation — 10 to 25% regression",[393,1625,1627],{"x":395,"y":1626,"fill":405,"style":438},"158","appears in the diff view, does not block the merge",[381,1629],{"x":1309,"y":1630,"width":1605,"height":403,"rx":1313,"fill":1365,"opacity":1315,"stroke":415,"style":406},"176",[393,1632,1634],{"x":395,"y":1633,"fill":397,"style":1610},"198","Failed check — above 25%",[393,1636,1638],{"x":395,"y":1637,"fill":405,"style":438},"218","blocks the merge, names the median it was compared against",[393,1640,1641],{"x":1309,"y":1328,"fill":405,"style":451},"A comment on every pull request is the fourth option, and the one that gets muted within a month",[458,1643,1644],{},"The summary is what makes the gate trustworthy: when it does fail, the number it failed on has been visible on every run leading up to it.",[27,1646,1648],{"id":1647},"measured-impact","Measured Impact",[14,1650,1651],{},"An Eleventy documentation site tracked for four quarters after adding the gate:",[1653,1654,1655,1677],"table",{},[1656,1657,1658],"thead",{},[1659,1660,1661,1665,1668,1671,1674],"tr",{},[1662,1663,1664],"th",{},"Quarter",[1662,1666,1667],{},"Pages",[1662,1669,1670],{},"Build time",[1662,1672,1673],{},"ms\u002Fpage",[1662,1675,1676],{},"Regressions caught",[1678,1679,1680,1698,1715,1732],"tbody",{},[1659,1681,1682,1686,1689,1692,1695],{},[1683,1684,1685],"td",{},"Q1 (baseline)",[1683,1687,1688],{},"620",[1683,1690,1691],{},"18.4 s",[1683,1693,1694],{},"29.7",[1683,1696,1697],{},"—",[1659,1699,1700,1703,1706,1709,1712],{},[1683,1701,1702],{},"Q2",[1683,1704,1705],{},"810",[1683,1707,1708],{},"23.9 s",[1683,1710,1711],{},"29.5",[1683,1713,1714],{},"2 (both reverted in the PR)",[1659,1716,1717,1720,1723,1726,1729],{},[1683,1718,1719],{},"Q3",[1683,1721,1722],{},"1,040",[1683,1724,1725],{},"31.2 s",[1683,1727,1728],{},"30.0",[1683,1730,1731],{},"1 (plugin swapped)",[1659,1733,1734,1737,1740,1743,1746],{},[1683,1735,1736],{},"Q4",[1683,1738,1739],{},"1,310",[1683,1741,1742],{},"39.1 s",[1683,1744,1745],{},"29.8",[1683,1747,1748],{},"3 (2 reverted, 1 accepted)",[14,1750,1751],{},"Total build time rose 112% across the year, and milliseconds per page moved by less than 2%. That is the outcome the gate exists to produce: the build got slower only because the site got bigger, and each of the six genuine regressions was caught in the pull request that introduced it rather than a year later.",[27,1753,1755],{"id":1754},"pitfalls-rollback","Pitfalls & Rollback",[32,1757,1758,1764,1770,1776,1782,1788],{},[35,1759,1760,1763],{},[1394,1761,1762],{},"Timing the whole job."," Install, tests and deploy swamp the signal and vary with runner contention.",[35,1765,1766,1769],{},[1394,1767,1768],{},"Comparing cold against warm."," A cache miss is not a regression; record cache state and compare like with like.",[35,1771,1772,1775],{},[1394,1773,1774],{},"Absolute thresholds."," Five seconds means something different on a 20-second build than on a four-minute one.",[35,1777,1778,1781],{},[1394,1779,1780],{},"Comparing against the previous run."," Runner variance guarantees false positives; use a rolling median.",[35,1783,1784,1787],{},[1394,1785,1786],{},"Failing on small regressions."," A gate that fires often gets disabled. Warn below 25%, fail above it.",[35,1789,1790,1793],{},[1394,1791,1792],{},"Rollback:"," the check is one script and one workflow step. Removing the step restores the previous pipeline, and the history file is inert data that can simply be deleted.",[27,1795,1797],{"id":1796},"conclusion","Conclusion",[14,1799,1800,1801,25],{},"Build times decay by accumulation, so the only defence is a number that someone sees on every pull request. Time the build step alone, normalise by page count, compare against a rolling median of comparable runs, and reserve a hard failure for regressions large enough to be real. When it fires, check page count, then dependencies, then templates — in that order, because that is the order of how often each is the cause. The caching and incrementality that keep the number low in the first place are in ",[21,1802,24],{"href":23},[27,1804,1806],{"id":1805},"faq","FAQ",[1808,1809,1811],"h3",{"id":1810},"why-not-just-watch-the-ci-job-duration","Why not just watch the CI job duration?",[14,1813,1814],{},"Because job duration mixes checkout, dependency install, build, tests and deploy, and it varies with runner contention. Timing the build step alone, and recording it as a number rather than reading it from a log, is what makes a trend visible.",[1808,1816,1818],{"id":1817},"how-do-i-stop-runner-variance-from-causing-false-alarms","How do I stop runner variance from causing false alarms?",[14,1820,1821],{},"Compare against a rolling median of recent runs on the default branch rather than against the previous run, and set the threshold as a percentage rather than a fixed number of seconds. A 25 percent regression against a 20-run median is a real signal; a 3-second jump against one previous run is noise.",[1808,1823,1825],{"id":1824},"should-a-build-time-regression-fail-the-pull-request","Should a build-time regression fail the pull request?",[14,1827,1828],{},"Fail it for a large regression and warn for a small one. A hard failure at 25 percent catches an accidental quadratic template; a warning at 10 percent keeps the number visible without blocking work that legitimately adds pages.",[1808,1830,1832],{"id":1831},"how-do-i-separate-a-cache-miss-from-a-real-slowdown","How do I separate a cache miss from a real slowdown?",[14,1834,1835],{},"Record whether the cache was restored as part of the measurement, and compare warm builds against warm builds. A cold run after a dependency change is expected to be slow and should never be compared against a warm baseline.",[1808,1837,1839],{"id":1838},"what-should-i-do-when-the-site-legitimately-gets-bigger","What should I do when the site legitimately gets bigger?",[14,1841,1842],{},"Normalise by page count. Tracking milliseconds per page instead of total seconds means adding 200 pages does not look like a regression, while a template that got slower still does.",[27,1844,1846],{"id":1845},"related","Related",[32,1848,1849,1858,1863,1870,1877],{},[35,1850,1851,1854,1855,1857],{},[1394,1852,1853],{},"Parent:"," ",[21,1856,24],{"href":23}," — the levers that keep the number low.",[35,1859,1860,1862],{},[21,1861,358],{"href":357}," — finding the template behind a regression.",[35,1864,1865,1869],{},[21,1866,1868],{"href":1867},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners\u002F","Sharing Build Cache Across CI Runners"," — why cache state must be part of the measurement.",[35,1871,1872,1876],{},[21,1873,1875],{"href":1874},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds\u002F","Caching node_modules in GitHub Actions for Faster SSG Builds"," — the other half of a warm build.",[35,1878,1879,1883],{},[21,1880,1882],{"href":1881},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow\u002F","Deploying to Multiple Environments From One Workflow"," — where this step lives in the pipeline.",[1885,1886,1887],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":57,"searchDepth":72,"depth":72,"links":1889},[1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1906],{"id":29,"depth":72,"text":30},{"id":46,"depth":72,"text":47},{"id":463,"depth":72,"text":464},{"id":1061,"depth":72,"text":1062},{"id":1386,"depth":72,"text":1387},{"id":1463,"depth":72,"text":1464},{"id":1647,"depth":72,"text":1648},{"id":1754,"depth":72,"text":1755},{"id":1796,"depth":72,"text":1797},{"id":1805,"depth":72,"text":1806,"children":1900},[1901,1902,1903,1904,1905],{"id":1810,"depth":78,"text":1811},{"id":1817,"depth":78,"text":1818},{"id":1824,"depth":78,"text":1825},{"id":1831,"depth":78,"text":1832},{"id":1838,"depth":78,"text":1839},{"id":1845,"depth":72,"text":1846},[1908,1910,1913,1914],{"name":1909,"item":785},"Home",{"name":1911,"item":1912},"Production-Ready Deployment & CI\u002FCD Workflows","\u002Fproduction-ready-deployment-cicd-workflows\u002F",{"name":24,"item":23},{"name":5,"item":1915},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fmeasuring-build-time-regressions-in-ci\u002F","2026-08-01","Track build duration per commit, separate cache misses from real slowdowns, and set a budget that fails a pull request before a 4-second build becomes a 4-minute one.","md",[1920,1921,1922,1923,1924],{"q":1811,"a":1814},{"q":1818,"a":1821},{"q":1825,"a":1828},{"q":1832,"a":1835},{"q":1839,"a":1842},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fmeasuring-build-time-regressions-in-ci",{"title":5,"description":1917},"production-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fmeasuring-build-time-regressions-in-ci\u002Findex","article","eW_zPywygBsp1YrtimmZxm005QfvQSjdLqh40aAxGdU",[1932,1935,1938,1941,1944,1947,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2122,2125,2128,2131,2134,2136,2137,2139,2142,2145,2148,2151,2154,2157,2160,2163,2166,2169,2172],{"path":1933,"title":1934},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fchoosing-between-astro-and-eleventy-for-large-docs","Astro vs Eleventy for Large Docs (1000+ Pages)",{"path":1936,"title":1937},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fcontent-collections-vs-eleventy-data-cascade","Content Collections vs the Eleventy Data Cascade",{"path":1939,"title":1940},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites","Astro vs Eleventy for Documentation Sites",{"path":1942,"title":1943},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhow-to-benchmark-hugo-vs-astro-build-speeds","How to Benchmark Hugo vs Astro Build Speeds",{"path":1945,"title":1946},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories","Hugo Build Times for Large Repositories",{"path":1948,"title":358},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fprofiling-hugo-templates-with-template-metrics",{"path":1950,"title":1951},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fspeeding-up-hugo-builds-with-render-hooks-and-caching","Speeding Up Hugo Builds with Render Hooks & Caching",{"path":1953,"title":1954},"\u002Fchoosing-the-right-static-site-generator-for-production","Choosing the Right Static Site Generator for Production",{"path":1956,"title":1957},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Feleventy-vs-jekyll-for-markdown-heavy-blogs","Eleventy vs Jekyll for Markdown-Heavy Blogs",{"path":1959,"title":1960},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem","Jekyll Plugin Ecosystem",{"path":1962,"title":1963},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Freplacing-jekyll-plugins-when-migrating-to-eleventy","Replacing Jekyll Plugins When Migrating to Eleventy",{"path":1965,"title":1966},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Frunning-jekyll-on-github-pages-without-plugins","Running Jekyll on GitHub Pages Without Plugins",{"path":1968,"title":1969},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators","Migrating Between Static Site Generators",{"path":1971,"title":1972},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fkeeping-redirects-working-after-an-ssg-migration","Keeping Redirects Working After an SSG Migration",{"path":1974,"title":1975},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fmigrating-a-docs-site-from-jekyll-to-hugo","Migrating a Docs Site From Jekyll to Hugo",{"path":1977,"title":1978},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fmigrating-from-hugo-to-astro-without-breaking-urls","Migrating From Hugo to Astro Without Breaking URLs",{"path":1980,"title":1981},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fporting-shortcodes-and-includes-between-generators","Porting Shortcodes and Includes Between Generators",{"path":1983,"title":1984},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fhandling-dynamic-routes-in-nextjs-static-export","Handling Dynamic Routes in Next.js Static Export",{"path":1986,"title":1987},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites","Next.js Static Export for Content Sites",{"path":1989,"title":1990},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fmigrating-from-gatsby-to-nextjs-static-export","Migrating from Gatsby to Next.js Static Export",{"path":1992,"title":1993},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-static-export-vs-astro-for-marketing-sites","Next.js Static Export vs Astro for Marketing",{"path":1995,"title":1996},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fbest-ssg-for-technical-writers-without-coding-experience","Best SSG for Non-Developer Technical Writers",{"path":1998,"title":1999},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fchoosing-an-ssg-for-api-reference-documentation","Choosing an SSG for API Reference Documentation",{"path":2001,"title":2002},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix","SSG Framework Selection Matrix",{"path":2004,"title":2005},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fpicking-an-ssg-for-a-multi-language-documentation-site","Picking an SSG for a Multi-Language Docs Site",{"path":2007,"title":2008},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams","SSG Selection Checklist for Engineering Teams",{"path":2010,"title":2011},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs","CDN Caching Rules for SSGs",{"path":2013,"title":2014},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fpurging-the-cdn-cache-after-a-static-deploy","Purging the CDN Cache After a Static Deploy",{"path":2016,"title":2017},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-cache-control-headers-on-cloudflare-pages","Cache-Control Headers on Cloudflare Pages",{"path":2019,"title":2020},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-up-proper-cache-headers-on-netlify","Proper Cache Headers on Netlify for SSGs",{"path":2022,"title":2023},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Feliminating-layout-shift-from-web-fonts","Eliminating Layout Shift From Web Fonts",{"path":2025,"title":2026},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Ffixing-cls-from-late-loading-embeds","Fixing CLS From Late-Loading Embeds",{"path":2028,"title":2029},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites","Cumulative Layout Shift Fixes for Static Sites",{"path":2031,"title":2032},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Fmeasuring-cls-in-the-field-with-web-vitals-js","Measuring CLS in the Field With web-vitals.js",{"path":2034,"title":2035},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Freserving-space-for-images-and-embeds-to-stop-layout-shift","Reserving Space for Images and Embeds",{"path":2037,"title":2038},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites","Font Loading Strategies for Static Sites",{"path":2040,"title":2041},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fself-hosting-google-fonts-to-eliminate-layout-shift","Self-Host Google Fonts to Eliminate Layout Shift",{"path":2043,"title":2044},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fsubsetting-variable-fonts-for-faster-first-render","Subsetting Variable Fonts for Faster First Render",{"path":2046,"title":2047},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fbuilding-an-image-cdn-pipeline-for-static-sites","Building an Image CDN Pipeline for Static Sites",{"path":2049,"title":2050},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro","Image Optimization Pipelines in Astro",{"path":2052,"title":2053},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Foptimizing-webp-images-in-hugo-without-plugins","Optimizing WebP Images in Hugo Without Plugins",{"path":2055,"title":2056},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fserving-avif-with-fallbacks-on-static-sites","Serving AVIF With Fallbacks on Static Sites",{"path":2058,"title":2059},"\u002Fperformance-optimization-core-web-vitals-for-ssgs","Core Web Vitals Optimization for SSGs",{"path":2061,"title":2062},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance","Astro Islands vs Full Hydration Performance",{"path":2064,"title":2065},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fhow-to-reduce-bundle-size-in-eleventy-builds","How to Reduce Bundle Size in Eleventy Builds",{"path":2067,"title":2068},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering","JavaScript Hydration & Partial Rendering",{"path":2070,"title":2071},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fmeasuring-inp-on-static-sites-with-real-user-monitoring","Measuring INP on Static Sites with RUM",{"path":2073,"title":2074},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Feliminating-render-blocking-css-on-static-sites","Eliminating Render-Blocking CSS on Static Sites",{"path":2076,"title":2077},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites","Largest Contentful Paint Optimization for Static Sites",{"path":2079,"title":2080},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Foptimizing-lcp-on-astro-with-priority-hints","Optimizing LCP on Astro with Priority Hints",{"path":2082,"title":2083},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Freducing-lcp-from-hero-images-on-static-sites","Reducing LCP from Hero Images on Static Sites",{"path":2085,"title":2086},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Fauditing-third-party-scripts-with-lighthouse","Auditing Third-Party Scripts With Lighthouse",{"path":2088,"title":2089},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites","Third-Party Script Performance on Static Sites",{"path":2091,"title":2092},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Flazy-loading-youtube-embeds-on-static-sites","Lazy-Loading YouTube Embeds on Static Sites",{"path":2094,"title":2095},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Fself-hosting-analytics-to-cut-third-party-requests","Self-Hosting Analytics to Cut Third-Party Requests",{"path":2097,"title":2098},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages","Automating Eleventy Deployments on Cloudflare Pages",{"path":2100,"title":2101},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fdeploying-hugo-to-cloudflare-pages-and-workers","Deploying Hugo to Cloudflare Pages and Workers",{"path":2103,"title":2104},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup","Cloudflare Pages Edge Caching Setup",{"path":2106,"title":2107},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fdocs-as-code-review-workflow-for-writers","Docs-as-Code Review Workflow for Writers",{"path":2109,"title":2110},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams","Content Workflows for Documentation Teams",{"path":2112,"title":2113},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fscheduling-content-publication-with-cron-triggered-builds","Scheduling Content Publication With Cron-Triggered Builds",{"path":2115,"title":2116},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fwiring-a-headless-cms-to-a-static-build","Wiring a Headless CMS to a Static Build",{"path":2118,"title":2119},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds","Caching node_modules in GitHub Actions",{"path":2121,"title":1882},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow",{"path":2123,"title":2124},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments","GitHub Actions for Hugo Deployments",{"path":2126,"title":2127},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds","GitHub Actions for Automated SSG Builds",{"path":2129,"title":2130},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fcaching-hugo-builds-in-github-actions","Caching Hugo Builds in GitHub Actions",{"path":2132,"title":2133},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy","Enabling Incremental Builds in Eleventy",{"path":2135,"title":24},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs",{"path":1926,"title":5},{"path":2138,"title":1868},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners",{"path":2140,"title":2141},"\u002Fproduction-ready-deployment-cicd-workflows","Production-Ready Deployment & CI\u002FCD for SSGs",{"path":2143,"title":2144},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies","Netlify vs Vercel Deployment Strategies",{"path":2146,"title":2147},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates","Netlify Build Hooks for Content Updates",{"path":2149,"title":2150},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fsetting-up-deploy-previews-on-netlify-for-every-pull-request","Netlify Deploy Previews for Every Pull Request",{"path":2152,"title":2153},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fvercel-isr-vs-static-generation-for-ssgs","Vercel ISR vs Static Generation for SSGs",{"path":2155,"title":2156},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions","Automating Preview Deploy Pipelines with GitHub Actions",{"path":2158,"title":2159},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments","Cleaning Up Stale Preview Deployments",{"path":2161,"title":2162},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests","Preview Environments for Pull Requests",{"path":2164,"title":2165},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Fatomic-deploys-vs-incremental-uploads","Atomic Deploys vs Incremental Uploads",{"path":2167,"title":2168},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites","Rollbacks and Deploy Safety for Static Sites",{"path":2170,"title":2171},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frolling-back-a-bad-static-deploy-in-under-a-minute","Rolling Back a Bad Static Deploy in Under a Minute",{"path":2173,"title":2174},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frunning-smoke-tests-against-a-preview-url","Running Smoke Tests Against a Preview URL",1785611671477]