[{"data":1,"prerenderedAt":848},["ShallowReactive",2],{"\u002Fblog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications":3,"\u002Fblog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications:related":832},{"id":4,"title":5,"author":6,"authorImage":7,"body":8,"category":817,"date":818,"description":819,"draft":820,"excerpt":821,"extension":822,"heroImage":823,"meta":824,"navigation":234,"path":825,"publishedAt":826,"readTime":827,"readingTime":821,"seo":828,"stem":829,"tags":830,"__hash__":831},"blog\u002Fblog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications.md","Measuring Largest Contentful Paint in Single-Page Applications","Florian Bücklers","\u002Fimages\u002Fauthors\u002Fflorian-buecklers.avif",{"type":9,"value":10,"toc":805},"minimark",[11,16,20,23,27,30,33,36,39,43,46,53,58,70,86,92,116,123,140,149,516,519,523,537,547,550,623,634,638,655,661,665,668,692,696,699,712,777,780,784,798,801],[12,13,15],"h2",{"id":14},"your-spa-is-faster-than-you-think-a-custom-solution-for-measuring-lcp","Your SPA Is Faster Than You Think: A Custom Solution for Measuring LCP",[17,18,19],"p",{},"You've shipped it. Your new Single-Page Application (SPA) is a marvel of modern engineering. Lighthouse scores are green, your bundle size is optimized, and yet... the feedback rolls in: \"It feels sluggish when I click around.\"",[17,21,22],{},"This frustrating disconnect between your metrics and your users' reality is a classic sign that your Core Web Vitals are lying to you. The culprit? Largest Contentful Paint (LCP), a metric fundamentally broken by the very nature of SPAs.",[12,24,26],{"id":25},"the-problem-lcp-and-the-blind-spot-of-soft-navigations","The Problem: LCP and the Blind Spot of \"Soft Navigations\"",[17,28,29],{},"LCP is the single most important metric for perceived loading speed. It marks the moment the main content of a page becomes visible to the user. For traditional websites, where each click triggers a full page load, this is easy to measure.",[17,31,32],{},"But SPAs are different. They use \"soft navigations\"—dynamically rewriting the page with JavaScript instead of requesting a new one from the server. To the browser's performance observers, your entire application is one long, single page load. It measures the LCP for the initial landing but becomes completely blind to the critical LCP of subsequent views loaded in-app.",[17,34,35],{},"This is a massive blind spot. If your \"products\" page takes four seconds to show its hero image after a user clicks from the homepage, that's a four-second LCP that is completely invisible to standard tools.",[17,37,38],{},"The Chrome team is aware of this and ran an initial origin trial to start reporting on soft navigation LCP. We eagerly tested it, but the trial was paused as they plan to improve it further before running another iteration. This created a gap: we needed to continue measuring the soft LCP for our customers, but the official solution was on hold. To fill this gap, we developed our own algorithm.",[12,40,42],{"id":41},"a-custom-solution-the-hunt-for-the-soft-lcp","A Custom Solution: The Hunt for the Soft LCP",[17,44,45],{},"To solve this LCP conundrum, we need to replicate the browser's own LCP detection logic, but scope it to a soft navigation. Our approach dives deep into the browser's rendering pipeline.",[17,47,48],{},[49,50],"img",{"alt":51,"src":52},"","\u002Fimages\u002Fblog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications\u002F1.webp",[54,55,57],"h3",{"id":56},"intercepting-image-creation","Intercepting Image Creation",[17,59,60,61,65,66,69],{},"To measure the soft LCP, we first need to identify potential LCP image candidates as they are being created by the SPA framework. The core of our algorithm achieves this by intercepting the ",[62,63,64],"code",{},"src"," property setter on the global ",[62,67,68],{},"HTMLImageElement.prototype",".",[17,71,72,73,77,78,81,82,85],{},"However, instrumenting ",[74,75,76],"em",{},"every"," image on a page - including tiny icons, spacers, and third-party tracking pixels - would be inefficient and noisy. The provided code is more intelligent. As seen in the ",[62,79,80],{},"setupSoftLCPObserver"," function, the mechanism is initialized with an ",[62,83,84],{},"imageUrls"," array. This acts as an allowlist.",[17,87,88,89,91],{},"Our custom ",[62,90,64],{}," setter only takes action if two conditions are met:",[93,94,95,107],"ol",{},[96,97,98,99,102,103,106],"li",{},"A soft navigation measurement is currently active (",[62,100,101],{},"observeImageElements"," is ",[62,104,105],{},"true",").",[96,108,109,110,112,113,115],{},"The image's ",[62,111,64],{}," URL contains one of the strings from the ",[62,114,84],{}," allowlist (e.g., your CDN's domain).",[17,117,118,119,122],{},"When a relevant image is identified, it is \"instrumented.\" This involves two critical steps performed by the ",[62,120,121],{},"observeImagePaint"," function in the code:",[93,124,125],{},[96,126,127,135,136,139],{},[128,129,130,131,134],"strong",{},"The ",[62,132,133],{},"elementtiming"," attribute is added to the image element."," This explicitly tells the browser to monitor this specific element and report its paint timing via the ",[62,137,138],{},"PerformanceElementTiming"," API.",[17,141,142,148],{},[128,143,144,145,69],{},"The element is added to a set of ",[62,146,147],{},"imageCandidates"," This set holds all the potential LCP elements for the current soft navigation, which are then passed to the paint-time approximation logic for further analysis.",[150,151,155],"pre",{"className":152,"code":153,"language":154,"meta":51,"style":51},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F Simplified from the main script for clarity\nconst originalImageSrcSetter = Object.getOwnPropertyDescriptor(\n  HTMLImageElement.prototype,\n  'src'\n)!.set!;\n\n\u002F\u002F The 'imageUrls' array is provided during setup\nconst imageUrls = [\"my-cdn.domain.com\", \"other-images.domain.com\"];\nlet observeImageElements = false; \u002F\u002F This is true only during a soft nav\n\nfunction observeImagePaint(image, src) {\n  \u002F\u002F Only act on relevant images during a soft navigation\n  if (observeImageElements && imageUrls.some(matcher => src.includes(matcher))) {\n    \u002F\u002F 1. Opt the image into PerformanceElementTiming\n    image.setAttribute(\"elementtiming\", \"\");\n\n    \u002F\u002F 2. Add to a list for paint time analysis\n    \u002F\u002F imageCandidates.add(image);\n    \u002F\u002F ... logic to start observing for paints ...\n  }\n}\n\nObject.defineProperty(HTMLImageElement.prototype, 'src', {\n  set(src) {\n    \u002F\u002F Our custom logic runs first\n    observeImagePaint(this, src);\n    \u002F\u002F Then, we call the original setter to maintain default browser behavior\n    originalImageSrcSetter.call(this, src);\n  },\n});\n","ts",[62,156,157,166,191,205,212,229,236,242,267,288,293,316,322,357,363,385,390,396,402,408,414,420,425,451,463,469,483,489,504,510],{"__ignoreMap":51},[158,159,162],"span",{"class":160,"line":161},"line",1,[158,163,165],{"class":164},"sJ8bj","\u002F\u002F Simplified from the main script for clarity\n",[158,167,169,173,177,180,184,188],{"class":160,"line":168},2,[158,170,172],{"class":171},"szBVR","const",[158,174,176],{"class":175},"sj4cs"," originalImageSrcSetter",[158,178,179],{"class":171}," =",[158,181,183],{"class":182},"sVt8B"," Object.",[158,185,187],{"class":186},"sScJk","getOwnPropertyDescriptor",[158,189,190],{"class":182},"(\n",[158,192,194,197,199,202],{"class":160,"line":193},3,[158,195,196],{"class":175},"  HTMLImageElement",[158,198,69],{"class":182},[158,200,201],{"class":175},"prototype",[158,203,204],{"class":182},",\n",[158,206,208],{"class":160,"line":207},4,[158,209,211],{"class":210},"sZZnC","  'src'\n",[158,213,215,218,221,224,226],{"class":160,"line":214},5,[158,216,217],{"class":182},")",[158,219,220],{"class":171},"!",[158,222,223],{"class":182},".set",[158,225,220],{"class":171},[158,227,228],{"class":182},";\n",[158,230,232],{"class":160,"line":231},6,[158,233,235],{"emptyLinePlaceholder":234},true,"\n",[158,237,239],{"class":160,"line":238},7,[158,240,241],{"class":164},"\u002F\u002F The 'imageUrls' array is provided during setup\n",[158,243,245,247,250,252,255,258,261,264],{"class":160,"line":244},8,[158,246,172],{"class":171},[158,248,249],{"class":175}," imageUrls",[158,251,179],{"class":171},[158,253,254],{"class":182}," [",[158,256,257],{"class":210},"\"my-cdn.domain.com\"",[158,259,260],{"class":182},", ",[158,262,263],{"class":210},"\"other-images.domain.com\"",[158,265,266],{"class":182},"];\n",[158,268,270,273,276,279,282,285],{"class":160,"line":269},9,[158,271,272],{"class":171},"let",[158,274,275],{"class":182}," observeImageElements ",[158,277,278],{"class":171},"=",[158,280,281],{"class":175}," false",[158,283,284],{"class":182},"; ",[158,286,287],{"class":164},"\u002F\u002F This is true only during a soft nav\n",[158,289,291],{"class":160,"line":290},10,[158,292,235],{"emptyLinePlaceholder":234},[158,294,296,299,302,305,309,311,313],{"class":160,"line":295},11,[158,297,298],{"class":171},"function",[158,300,301],{"class":186}," observeImagePaint",[158,303,304],{"class":182},"(",[158,306,308],{"class":307},"s4XuR","image",[158,310,260],{"class":182},[158,312,64],{"class":307},[158,314,315],{"class":182},") {\n",[158,317,319],{"class":160,"line":318},12,[158,320,321],{"class":164},"  \u002F\u002F Only act on relevant images during a soft navigation\n",[158,323,325,328,331,334,337,340,342,345,348,351,354],{"class":160,"line":324},13,[158,326,327],{"class":171},"  if",[158,329,330],{"class":182}," (observeImageElements ",[158,332,333],{"class":171},"&&",[158,335,336],{"class":182}," imageUrls.",[158,338,339],{"class":186},"some",[158,341,304],{"class":182},[158,343,344],{"class":307},"matcher",[158,346,347],{"class":171}," =>",[158,349,350],{"class":182}," src.",[158,352,353],{"class":186},"includes",[158,355,356],{"class":182},"(matcher))) {\n",[158,358,360],{"class":160,"line":359},14,[158,361,362],{"class":164},"    \u002F\u002F 1. Opt the image into PerformanceElementTiming\n",[158,364,366,369,372,374,377,379,382],{"class":160,"line":365},15,[158,367,368],{"class":182},"    image.",[158,370,371],{"class":186},"setAttribute",[158,373,304],{"class":182},[158,375,376],{"class":210},"\"elementtiming\"",[158,378,260],{"class":182},[158,380,381],{"class":210},"\"\"",[158,383,384],{"class":182},");\n",[158,386,388],{"class":160,"line":387},16,[158,389,235],{"emptyLinePlaceholder":234},[158,391,393],{"class":160,"line":392},17,[158,394,395],{"class":164},"    \u002F\u002F 2. Add to a list for paint time analysis\n",[158,397,399],{"class":160,"line":398},18,[158,400,401],{"class":164},"    \u002F\u002F imageCandidates.add(image);\n",[158,403,405],{"class":160,"line":404},19,[158,406,407],{"class":164},"    \u002F\u002F ... logic to start observing for paints ...\n",[158,409,411],{"class":160,"line":410},20,[158,412,413],{"class":182},"  }\n",[158,415,417],{"class":160,"line":416},21,[158,418,419],{"class":182},"}\n",[158,421,423],{"class":160,"line":422},22,[158,424,235],{"emptyLinePlaceholder":234},[158,426,428,431,434,436,439,441,443,445,448],{"class":160,"line":427},23,[158,429,430],{"class":182},"Object.",[158,432,433],{"class":186},"defineProperty",[158,435,304],{"class":182},[158,437,438],{"class":175},"HTMLImageElement",[158,440,69],{"class":182},[158,442,201],{"class":175},[158,444,260],{"class":182},[158,446,447],{"class":210},"'src'",[158,449,450],{"class":182},", {\n",[158,452,454,457,459,461],{"class":160,"line":453},24,[158,455,456],{"class":186},"  set",[158,458,304],{"class":182},[158,460,64],{"class":307},[158,462,315],{"class":182},[158,464,466],{"class":160,"line":465},25,[158,467,468],{"class":164},"    \u002F\u002F Our custom logic runs first\n",[158,470,472,475,477,480],{"class":160,"line":471},26,[158,473,474],{"class":186},"    observeImagePaint",[158,476,304],{"class":182},[158,478,479],{"class":175},"this",[158,481,482],{"class":182},", src);\n",[158,484,486],{"class":160,"line":485},27,[158,487,488],{"class":164},"    \u002F\u002F Then, we call the original setter to maintain default browser behavior\n",[158,490,492,495,498,500,502],{"class":160,"line":491},28,[158,493,494],{"class":182},"    originalImageSrcSetter.",[158,496,497],{"class":186},"call",[158,499,304],{"class":182},[158,501,479],{"class":175},[158,503,482],{"class":182},[158,505,507],{"class":160,"line":506},29,[158,508,509],{"class":182},"  },\n",[158,511,513],{"class":160,"line":512},30,[158,514,515],{"class":182},"});\n",[17,517,518],{},"This targeted interception strategy is highly efficient. It ensures we only monitor the paint timing for meaningful content images, providing a clean and relevant set of candidates for determining the true soft navigation LCP.",[54,520,522],{"id":521},"approximating-paint-time","Approximating Paint Time",[17,524,525,526,528,529,532,533,536],{},"To accurately determine when an image is painted, our solution must overcome a key shortcoming of the ",[62,527,138],{}," API, especially within the dynamic lifecycle of an SPA. While this API is the ideal tool for observing an element's actual paint time, its behavior can be inconsistent if an image is already loaded ",[74,530,531],{},"before"," it's attached to the DOM. This can happen if the image is served from the browser's cache, or because modern browsers often start downloading an image as soon as its element is created in memory, even before it is added to the page. In these common scenarios, the browser may not emit an ",[62,534,535],{},"element"," timing entry, leaving us blind.",[17,538,539,540,543,544,69],{},"Our algorithm solves this with a robust, dual-pronged approach using ",[62,541,542],{},"requestAnimationFrame"," and ",[62,545,546],{},"IntersectionObserver",[17,548,549],{},"Here’s how it works step-by-step:",[93,551,552,558,571],{},[96,553,554,557],{},[128,555,556],{},"Identify Candidates:"," When the image creation interception logic detects a new image, it's added to a set of potential LCP candidates.",[96,559,560,563,564,566,567,570],{},[128,561,562],{},"Monitor Browser Paints:"," We then start a ",[62,565,542],{}," loop. This is critical because it allows us to execute code at the precise moment ",[74,568,569],{},"just before"," the browser performs a new paint.",[96,572,573,576,577,579,580,583,584,587,588,102,590,593,594,597,598,600,601,604,605,587,608,102,610,612,613,615,616,619,620,622],{},[128,574,575],{},"Check Image Status at Paint Time:"," Within each ",[62,578,542],{}," callback, we check our candidate images. For any image that has been connected to the DOM, we determine its loading status (",[62,581,582],{},"image.complete","). This check leads to two distinct paths: ",[128,585,586],{},"The Happy Path (Image Not Yet Loaded):"," If ",[62,589,582],{},[62,591,592],{},"false",", it means the image is still being fetched from the network when it's rendered. In this case, we can confidently rely on the standard ",[62,595,596],{},"PerformanceObserver"," to fire an ",[62,599,535],{}," timing entry. This entry will provide the most accurate ",[62,602,603],{},"renderTime"," once the image is fully loaded and painted. ",[128,606,607],{},"The Fallback Path (Image Already Loaded):",[62,609,582],{},[62,611,105],{},", we are in the problematic scenario. To capture this timing, we immediately register the element with an ",[62,614,546],{},". This observer will trigger as soon as the element becomes visible in the viewport, providing a high-resolution timestamp (",[62,617,618],{},"entry.time","). We use this timestamp as a very close and reliable approximation of its paint time.Once an image candidate is connected to the DOM and processed via one of these two paths, it is removed from our set of active candidates. As an optimization: The ",[62,621,542],{}," loop will automatically stop once all pending images have been handled, preventing any unnecessary monitoring.",[17,624,625,626,260,628,630,631,633],{},"This dual strategy ensures that no matter how or when an image is loaded, we can capture a precise and meaningful timestamp for when it is actually painted on the screen. By combining the strengths of ",[62,627,138],{},[62,629,542],{},", and ",[62,632,546],{},", we create a resilient system that accurately measures the paint time for LCP candidates in a soft navigation.",[54,635,637],{"id":636},"resource-timings-and-caching","Resource Timings and Caching",[17,639,640,641,644,645,260,648,630,651,654],{},"The script also observes ",[62,642,643],{},"resource"," timings to gather detailed attribution data for the LCP, such as ",[62,646,647],{},"loadDelay",[62,649,650],{},"loadDuration",[62,652,653],{},"renderDelay",". This is crucial for understanding the performance characteristics of the soft navigation.",[17,656,657,658,660],{},"However, a key consideration is browser caching. If an image has been previously loaded (even during a hard navigation), it might be served from the browser's in-memory cache. In such cases, no new resource timing will be generated, and the LCP will be entirely attributed to ",[62,659,653],{},". This is an important nuance to keep in mind when analyzing soft LCP data.",[54,662,664],{"id":663},"limitations-of-this-approach","Limitations of this Approach",[17,666,667],{},"While this algorithm provides a powerful way to approximate soft LCP, it's important to be aware of its limitations:",[669,670,671,677,686],"ul",{},[96,672,673,676],{},[128,674,675],{},"Text Nodes are Not Measured:"," This implementation focuses exclusively on image elements as LCP candidates. The standard LCP metric can also identify text blocks as the largest element. This algorithm does not account for text-based LCP, which might be the true LCP in some soft navigations.",[96,678,679,682,683,685],{},[128,680,681],{},"Browser Compatibility:"," The solution relies heavily on the ",[62,684,138],{}," API. As of now, this API is primarily supported in Chromium-based browsers (like Google Chrome and Microsoft Edge). Therefore, this LCP measurement technique will not work in browsers like Firefox or Safari.",[96,687,688,691],{},[128,689,690],{},"Scroll Restoration Issues:"," SPA frameworks often handle scroll restoration, attempting to return the user to their previous scroll position on a new view. This can create measurement challenges. For instance, when navigating back, content for the new page might start painting before the scroll position is reset. This can lead to LCP elements being reported as off-screen, as they are technically in the DOM but scrolled out of view by the restoration algorithm, potentially skewing the results.",[54,693,695],{"id":694},"framework-integration-for-accurate-timing","Framework Integration for Accurate Timing",[17,697,698],{},"For the most accurate LCP measurement, it's essential to signal the start of a soft navigation at the precise moment it begins. Relying on the History API is often too late, as many frameworks only update the history after the necessary resources have been loaded and the page is ready to render. This can lead to deceptively low LCP times.",[17,700,130,701,703,704,707,708,711],{},[62,702,80],{}," function returned by the script allows for an optional ",[62,705,706],{},"startTime"," to be passed to the ",[62,709,710],{},"triggerSoftNavigation"," function. This enables a more precise measurement based on the actual start of your navigation. Here's an example of how this can be implemented in a Nuxt.js application:",[150,713,717],{"className":714,"code":715,"language":716,"meta":51,"style":51},"language-js shiki shiki-themes github-light github-dark","let nuxt = window.useNuxtApp();\n\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fadvanced\u002Fhooks\n\nnuxt.hook(\"page:start\", () => triggerSoftNavigation(performance.now()));\n","js",[62,718,719,737,742,746],{"__ignoreMap":51},[158,720,721,723,726,728,731,734],{"class":160,"line":161},[158,722,272],{"class":171},[158,724,725],{"class":182}," nuxt ",[158,727,278],{"class":171},[158,729,730],{"class":182}," window.",[158,732,733],{"class":186},"useNuxtApp",[158,735,736],{"class":182},"();\n",[158,738,739],{"class":160,"line":168},[158,740,741],{"class":164},"\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fadvanced\u002Fhooks\n",[158,743,744],{"class":160,"line":193},[158,745,235],{"emptyLinePlaceholder":234},[158,747,748,751,754,756,759,762,765,768,771,774],{"class":160,"line":207},[158,749,750],{"class":182},"nuxt.",[158,752,753],{"class":186},"hook",[158,755,304],{"class":182},[158,757,758],{"class":210},"\"page:start\"",[158,760,761],{"class":182},", () ",[158,763,764],{"class":171},"=>",[158,766,767],{"class":186}," triggerSoftNavigation",[158,769,770],{"class":182},"(performance.",[158,772,773],{"class":186},"now",[158,775,776],{"class":182},"()));\n",[17,778,779],{},"By hooking directly into your framework's navigation lifecycle, you can achieve a much more accurate and representative measurement of your application's soft LCP, providing valuable insights into the user's perceived performance.",[12,781,783],{"id":782},"try-it-yourself","Try It Yourself",[17,785,786,787,796],{},"Ready to stop guessing and start measuring? You can find the complete, annotated code for this solution in the following GitHub Gist. We encourage you to experiment with it in your own SPA. ",[788,789,793],"a",{"href":790,"rel":791},"https:\u002F\u002Fgist.github.com\u002Ffbuecklers\u002Fcae61922e33ce99206dda105b6fd197d",[792],"nofollow",[128,794,795],{},"View the code on GitHub Gist",[128,797,69],{},[17,799,800],{},"Implement it, analyze the data you collect, and see what you discover about your application's soft navigation performance. What challenges did you face? How did this new insight change your optimization strategy? Share your findings and help push the conversation forward.",[802,803,804],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":51,"searchDepth":168,"depth":168,"links":806},[807,808,809,816],{"id":14,"depth":168,"text":15},{"id":25,"depth":168,"text":26},{"id":41,"depth":168,"text":42,"children":810},[811,812,813,814,815],{"id":56,"depth":193,"text":57},{"id":521,"depth":193,"text":522},{"id":636,"depth":193,"text":637},{"id":663,"depth":193,"text":664},{"id":694,"depth":193,"text":695},{"id":782,"depth":168,"text":783},"Web vitals","June 18, 2025","In this article, we will explain how to measure the Largest Contentful Paint (LCP) in Single-Page Applications (SPA).",false,null,"md","\u002Fimages\u002Fblog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications\u002Fhero.webp",{},"\u002Fblog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications","2025-06-18","8 min read",{"title":5,"description":819},"blog\u002Fmeasuring-largest-contentful-paint-in-single-page-applications",[],"J2Yla3AgG7nEVn8UrN12EmMW9KywBi8h_ToX5MDVxCU",[833,838,843],{"path":834,"title":835,"category":817,"date":836,"heroImage":837},"\u002Fblog\u002Fdont-settle-for-good-it-is-time-for-new-lcp-thresholds","Don’t Settle for “Good”: It Is Time for New LCP Thresholds","September 5, 2025","\u002Fimages\u002Fblog\u002Fdont-settle-for-good-it-is-time-for-new-lcp-thresholds\u002Fhero.webp",{"path":839,"title":840,"category":817,"date":841,"heroImage":842},"\u002Fblog\u002Fenhancing-web-responsiveness-how-to-debug-and-avoid-slow-interactions","Enhancing web responsiveness: How to debug and avoid slow interactions","September 3, 2024","\u002Fimages\u002Fblog\u002Fenhancing-web-responsiveness-how-to-debug-and-avoid-slow-interactions\u002Fhero.avif",{"path":844,"title":845,"category":817,"date":846,"heroImage":847},"\u002Fblog\u002Fcommon-causes-of-layout-shifts-and-how-to-fix-them-for-better-cls","Common causes of layout shifts and how to fix them for better CLS","August 24, 2023","\u002Fimages\u002Fblog\u002Fcommon-causes-of-layout-shifts-and-how-to-fix-them-for-better-cls\u002Fhero.avif",1788155763408]