[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-performanceobserver-core-web-vitals":93,"related-performanceobserver-core-web-vitals":299,"code:js:true:1ds7j14":428,"code:js:true:alme61":429,"code:js:true:et4qvs":430,"code:js:true:11qhxtu":431,"code:js:true:7vkjwc":432,"code:js:true:7dc1hh":433,"comments-01a11f48-c748-7368-a51b-ccd1d636ad0c":434},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,[45,49,53,57,61,65,69,73,77,81,85,89],{"slug":46,"name":47,"articles":48},"webdev","Webdev",138,{"slug":50,"name":51,"articles":52},"javascript","Javascript",113,{"slug":54,"name":55,"articles":56},"frontend","Frontend",83,{"slug":58,"name":59,"articles":60},"tutorial","Tutorial",53,{"slug":62,"name":63,"articles":64},"css","Css",42,{"slug":66,"name":67,"articles":68},"performance","Performance",21,{"slug":70,"name":71,"articles":72},"typescript","Typescript",18,{"slug":74,"name":75,"articles":76},"react","React",17,{"slug":78,"name":79,"articles":80},"browser","Browser",13,{"slug":82,"name":83,"articles":84},"node","Node",12,{"slug":86,"name":87,"articles":88},"programming","Programming",10,{"slug":90,"name":91,"articles":92},"accessibility","Accessibility",9,{"id":94,"slug":95,"title":96,"subtitle":97,"excerpt":98,"coverUrl":99,"locale":13,"readingMinutes":100,"publishedAt":101,"viewCount":102,"likeCount":19,"commentCount":19,"author":103,"vertical":108,"topic":109,"tags":111,"_count":116,"playground":118,"body":120,"bodyMd":283,"seo":284,"translationGroupId":287,"series":97,"podcastUrl":97,"verticalId":5,"thread":288,"assessments":290,"translations":295,"quiz":297},"01a11f48-c748-7368-a51b-ccd1d636ad0c","performanceobserver-core-web-vitals","You Installed a Library for Numbers PerformanceObserver Already Has.",null,"The web-vitals npm package gets credit for data your browser already computes. Here's what three PerformanceObserver calls give you for free — and the one piece of math it quietly saves you from getting wrong.","\u002Fmedia\u002Fcovers\u002Fperformanceobserver-core-web-vitals.png",7,"2026-10-14T06:10:28.030Z",41,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":50,"name":110},"JavaScript",[112,113,114,115],{"slug":50,"name":51,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":86,"name":87,"color":97},{"assessments":117},1,{"slug":95,"title":119},"CLS session windows — interactive playground",{"blocks":121,"version":117},[122,126,129,132,136,139,145,148,151,154,157,160,164,167,170,173,176,180,183,186,189,192,195,198,201,204,208,211,214,218,221,224,227,230,234,237,240,243,246,249,252,255,258,265,268,271,274,277],{"id":123,"html":124,"type":125},"b1","\u003Cp>Your bundle analyzer flags a new dependency after a routine \u003Ccode>npm install\u003C\u002Fcode>. Two kilobytes, not huge, but you go looking anyway. It&#39;s \u003Ccode>web-vitals\u003C\u002Fcode> — the package half the performance tutorials on the internet tell you to install just so you can \u003Ccode>console.log\u003C\u002Fcode> three numbers: LCP, CLS, INP.\u003C\u002Fp>","paragraph",{"id":127,"html":128,"type":125},"b2","\u003Cp>You open its source expecting something clever. It&#39;s mostly \u003Ccode>PerformanceObserver\u003C\u002Fcode> calls. The exact same API already sitting in your browser, for free, before your bundle even finishes downloading.\u003C\u002Fp>",{"id":130,"html":131,"type":125},"b3","\u003Cp>So why does nearly everyone reach for the library first? There&#39;s exactly one part of this that&#39;s genuinely hard to get right by hand — and it&#39;s not the part most people worry about.\u003C\u002Fp>",{"id":133,"html":134,"text":134,"type":135,"level":31},"b4","Measuring LCP by hand takes four lines","heading",{"id":137,"html":138,"type":125},"b5","\u003Cp>Largest Contentful Paint is the easy one. The browser already tracks which element painted the biggest visible chunk of content, and it hands you updates as a better candidate shows up:\u003C\u002Fp>",{"id":140,"code":141,"type":142,"language":143,"highlight":144},"b6","new PerformanceObserver((list) => {\n  const entries = list.getEntries();\n  const lastEntry = entries[entries.length - 1];\n  console.log(\"LCP so far:\", lastEntry.startTime, lastEntry.element);\n}).observe({ type: \"largest-contentful-paint\", buffered: true });","code","js",[],{"id":146,"html":147,"type":125},"b7","\u003Cp>\u003Ccode>buffered: true\u003C\u002Fcode> is the part people forget and then can&#39;t figure out why their observer logs nothing — it backfills entries that happened \u003Cem>before\u003C\u002Fem> this script ran, which for LCP is usually all of them, since your analytics script rarely loads before the hero image paints.\u003C\u002Fp>",{"id":149,"html":150,"type":125},"b8","\u003Cp>The browser keeps revising &quot;largest so far&quot; as bigger elements paint in, and stops for good the moment the user scrolls, clicks, or types, or the tab goes hidden — at that point, whatever the last entry said is the final score. \u003Ccode>entry.element\u003C\u002Fcode> is the actual DOM node, which is a nice touch for debugging (&quot;oh, the LCP element is the hidden carousel slide, not the headline&quot;) — except when it&#39;s \u003Ccode>null\u003C\u002Fcode>, which happens if the element has already been removed from the DOM by the time your callback runs. (A separate gotcha hits cross-origin images without a \u003Ccode>Timing-Allow-Origin\u003C\u002Fcode> header: \u003Ccode>element\u003C\u002Fcode> still resolves, but \u003Ccode>renderTime\u003C\u002Fcode> reads as \u003Ccode>0\u003C\u002Fcode>, so you have to fall back to \u003Ccode>loadTime\u003C\u002Fcode> instead — a different field going quiet, not the element.)\u003C\u002Fp>",{"id":152,"html":153,"type":125},"b9","\u003Cp>Four lines, one footgun, done. This is not the part \u003Ccode>web-vitals\u003C\u002Fcode> is earning its keep on.\u003C\u002Fp>",{"id":155,"html":156,"text":156,"type":135,"level":31},"b10","Where the naive version lies to you: CLS",{"id":158,"html":159,"type":125},"b11","\u003Cp>Cumulative Layout Shift reports as a \u003Ccode>layout-shift\u003C\u002Fcode> entry every time something visible moves without the user asking it to. Each entry has a \u003Ccode>.value\u003C\u002Fcode> — how much of the viewport shifted, weighted by distance. The obvious thing to do is sum them:\u003C\u002Fp>",{"id":161,"code":162,"type":142,"language":143,"highlight":163},"b12","let clsTotal = 0;\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.hadRecentInput) clsTotal += entry.value;\n  }\n  console.log(\"CLS so far:\", clsTotal);\n}).observe({ type: \"layout-shift\", buffered: true });",[],{"id":165,"html":166,"type":125},"b13","\u003Cp>(\u003Ccode>hadRecentInput\u003C\u002Fcode> is true if the shift happened within 500ms of the user clicking or typing — an accordion that resizes after you click it shouldn&#39;t count against you, so the spec excludes it.)\u003C\u002Fp>",{"id":168,"html":169,"type":125},"b14","\u003Cp>Ship that on a dashboard that stays open all day, with an ad slot that occasionally reflows, and watch \u003Ccode>clsTotal\u003C\u002Fcode> climb to 3, then 6, then double digits by the afternoon. Nothing on the page ever visibly jumped badly — a few small shifts, hours apart. The number is technically &quot;correct&quot; by your own code, and it is nowhere close to what the real metric reports, because real CLS was never meant to be a lifetime total.\u003C\u002Fp>",{"id":171,"html":172,"type":125},"b15","\u003Cp>\u003Cstrong>Before you scroll:\u003C\u002Fstrong> what do you think the actual rule is? A cap? A time window? A per-minute budget?\u003C\u002Fp>",{"id":174,"html":175,"type":125},"b16","\u003Cp>It&#39;s a \u003Cem>session window\u003C\u002Fem>. Group shifts where the gap since the last one is under 1 second, cap each group at 5 seconds total, and report whichever single window ever added up to the most:\u003C\u002Fp>",{"id":177,"code":178,"type":142,"language":143,"highlight":179},"b17","let windows = [];\nlet current = null;\n\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (entry.hadRecentInput) continue;\n\n    const since = entry.startTime - (current?.last ?? -Infinity);\n    const span = entry.startTime - (current?.start ?? entry.startTime);\n\n    if (current && since \u003C 1000 && span \u003C 5000) {\n      current.sum += entry.value;\n      current.last = entry.startTime;\n    } else {\n      current = { start: entry.startTime, last: entry.startTime, sum: entry.value };\n      windows.push(current);\n    }\n  }\n  const cls = Math.max(0, ...windows.map((w) => w.sum));\n  console.log(\"CLS (session-windowed):\", cls);\n}).observe({ type: \"layout-shift\", buffered: true });",[],{"id":181,"html":182,"type":125},"b18","\u003Cp>A burst of shifts from one bad ad reflow — all within a second or two of each other — stays grouped into one window and still hurts your score, correctly. But five unrelated shifts scattered across an afternoon never combine into one number, because they never do to a real visitor either: nobody experiences &quot;all the jank from today,&quot; they experience one moment where the page jumped under their thumb.\u003C\u002Fp>",{"id":184,"html":185,"type":125},"b19","\u003C!-- playground:start -->",{"id":187,"html":188,"text":188,"type":135,"level":31},"b20","🎮 Try it yourself",{"id":190,"html":191,"type":125},"b21","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fperformanceobserver-core-web-vitals\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":193,"html":194,"type":125},"b22","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":196,"html":197,"type":125},"b23","\u003C!-- playground:end -->",{"id":199,"html":200,"text":200,"type":135,"level":31},"b24","INP has the same trap, with an extra twist",{"id":202,"html":203,"type":125},"b25","\u003Cp>The naive move for Interaction to Next Paint is to listen for \u003Ccode>event\u003C\u002Fcode> entries and log every \u003Ccode>duration\u003C\u002Fcode>:\u003C\u002Fp>",{"id":205,"code":206,"type":142,"language":143,"highlight":207},"b26","new PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(entry.name, entry.duration);\n  }\n}).observe({ type: \"event\", buffered: true, durationThreshold: 16 });",[],{"id":209,"html":210,"type":125},"b27","\u003Cp>Tap a button on a touchscreen and this fires three or four times — \u003Ccode>pointerdown\u003C\u002Fcode>, \u003Ccode>pointerup\u003C\u002Fcode>, \u003Ccode>click\u003C\u002Fcode>, sometimes more — for what the user experienced as \u003Cem>one\u003C\u002Fem> tap. Log each \u003Ccode>duration\u003C\u002Fcode> as its own interaction and you&#39;ve just told your dashboard there were four slow interactions where there was one.\u003C\u002Fp>",{"id":212,"html":213,"type":125},"b28","\u003Cp>The fix is grouping: every \u003Ccode>event\u003C\u002Fcode> entry belonging to the same physical interaction shares an \u003Ccode>interactionId\u003C\u002Fcode>, so you group by that and take the worst duration inside each group — one number per real interaction, not one per DOM event:\u003C\u002Fp>",{"id":215,"code":216,"type":142,"language":143,"highlight":217},"b29","const interactions = new Map();\n\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.interactionId) continue; \u002F\u002F 0 means \"not a tracked interaction\"\n    const existing = interactions.get(entry.interactionId) ?? 0;\n    interactions.set(entry.interactionId, Math.max(existing, entry.duration));\n  }\n}).observe({ type: \"event\", buffered: true, durationThreshold: 16 });",[],{"id":219,"html":220,"type":125},"b30","\u003Cp>Even that map isn&#39;t the final INP score — the real metric doesn&#39;t just report \u003Ccode>Math.max(...interactions.values())\u003C\u002Fcode> either. A single freak slow tap (a GC pause at the wrong millisecond) shouldn&#39;t define the whole page&#39;s score any more than one slow HTTP request should define your p50 latency. The reference implementation tracks a high percentile across the session instead of the single worst value once there have been enough interactions to make a percentile meaningful — which is a few lines of arithmetic you do not want to be debugging for the first time during an incident.\u003C\u002Fp>",{"id":222,"html":223,"text":223,"type":135,"level":31},"b31","So what is the library actually for?",{"id":225,"html":226,"type":125},"b32","\u003Cp>Not the measuring — you just watched that happen in nine lines. It&#39;s the three things nobody gets right the first time they hand-roll this: the CLS session-windowing, the INP interaction-grouping-plus-percentile, and the edge cases around bfcache restores and visibility-change timing that silently double-report or under-report a metric if you don&#39;t special-case them. That&#39;s real, fiddly logic, and \u003Ccode>web-vitals\u003C\u002Fcode> has had years of bug reports to get it right. If you&#39;re sending these numbers to a real-user-monitoring pipeline that someone makes decisions from, use the library for exactly that math.\u003C\u002Fp>",{"id":228,"html":229,"type":125},"b33","\u003Cp>Here&#39;s the honest caveat, and it&#39;s narrower than it would have been a year ago: Safari was the last of the three engines to ship \u003Ccode>largest-contentful-paint\u003C\u002Fcode> and interaction-level \u003Ccode>event\u003C\u002Fcode> timing (the \u003Ccode>interactionId\u003C\u002Fcode> grouping above), adding both in Safari 26.2, December 2025. Firefox isn&#39;t new to this — it&#39;s had \u003Ccode>largest-contentful-paint\u003C\u002Fcode> since version 122 (early 2024) — but it only picked up \u003Ccode>interactionId\u003C\u002Fcode> around the same time as Safari, in Firefox 144. With Safari&#39;s December 2025 release, all three engines finally agreed on both entry types, which is what pushed web.dev to mark LCP and INP &quot;Baseline: Newly available&quot; at that point. \u003Ccode>layout-shift\u003C\u002Fcode> didn&#39;t make the trip — Cumulative Layout Shift is still Chromium-only; Firefox&#39;s tracking bug for it has sat open since 2021, and Safari 26.2 shipped the other two without it. So CLS is the one metric where neither the raw API nor \u003Ccode>web-vitals\u003C\u002Fcode> can produce a number for a Firefox or Safari visitor — the library&#39;s job there is still correctness on the browsers that \u003Cem>do\u003C\u002Fem> report it, not conjuring data from the one that doesn&#39;t. Feature-detect before you rely on any of this:\u003C\u002Fp>",{"id":231,"code":232,"type":142,"language":143,"highlight":233},"b34","if (PerformanceObserver.supportedEntryTypes.includes(\"layout-shift\")) {\n  \u002F\u002F measure it\n} else {\n  \u002F\u002F this visitor is on a browser that can't tell you\n}",[],{"id":235,"html":236,"type":125},"b35","\u003C!-- quiz:start -->",{"id":238,"html":239,"text":239,"type":135,"level":31},"b36","🧠 Test yourself",{"id":241,"html":242,"type":125},"b37","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fperformanceobserver-core-web-vitals\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":244,"html":245,"type":125},"b38","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":247,"html":248,"type":125},"b39","\u003C!-- quiz:end -->",{"id":250,"html":251,"type":125},"b40","\u003Cp>The library isn&#39;t buying you the measurement — the browser was always going to hand you that, Chromium or nothing. It&#39;s buying you the arithmetic that&#39;s easy to get wrong exactly once, in production, where you won&#39;t notice until the dashboard looks wrong for a month. Have you ever shipped a CLS or INP number you&#39;d now bet was quietly miscounted? I&#39;d genuinely like to know in the comments.\u003C\u002Fp>",{"id":253,"html":254,"type":125},"b41","\u003C!-- related:start -->",{"id":256,"html":257,"text":257,"type":135,"level":31},"b42","📚 Read next",{"id":259,"type":260,"items":261,"ordered":18},"b43","list",[262,263,264],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers\">Web Workers in JavaScript: The Complete Guide\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api\">Your Lighthouse Score Is 100. Long Animation Frames Explain the Jank.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components\">Shadow DOM Needed JavaScript to Exist. Now It Doesn&#39;t.\u003C\u002Fa>",{"id":266,"html":267,"type":125},"b44","\u003C!-- related:end -->",{"id":269,"type":270},"b45","divider",{"id":272,"html":273,"type":125},"b46","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":275,"html":276,"type":125},"b47","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":278,"type":260,"items":279,"ordered":18},"b48",[280,281,282],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Your bundle analyzer flags a new dependency after a routine `npm install`. Two kilobytes, not huge, but you go looking anyway. It's `web-vitals` — the package half the performance tutorials on the internet tell you to install just so you can `console.log` three numbers: LCP, CLS, INP.\n\nYou open its source expecting something clever. It's mostly `PerformanceObserver` calls. The exact same API already sitting in your browser, for free, before your bundle even finishes downloading.\n\nSo why does nearly everyone reach for the library first? There's exactly one part of this that's genuinely hard to get right by hand — and it's not the part most people worry about.\n\n## Measuring LCP by hand takes four lines\n\nLargest Contentful Paint is the easy one. The browser already tracks which element painted the biggest visible chunk of content, and it hands you updates as a better candidate shows up:\n\n```js\nnew PerformanceObserver((list) => {\n  const entries = list.getEntries();\n  const lastEntry = entries[entries.length - 1];\n  console.log(\"LCP so far:\", lastEntry.startTime, lastEntry.element);\n}).observe({ type: \"largest-contentful-paint\", buffered: true });\n```\n\n`buffered: true` is the part people forget and then can't figure out why their observer logs nothing — it backfills entries that happened *before* this script ran, which for LCP is usually all of them, since your analytics script rarely loads before the hero image paints.\n\nThe browser keeps revising \"largest so far\" as bigger elements paint in, and stops for good the moment the user scrolls, clicks, or types, or the tab goes hidden — at that point, whatever the last entry said is the final score. `entry.element` is the actual DOM node, which is a nice touch for debugging (\"oh, the LCP element is the hidden carousel slide, not the headline\") — except when it's `null`, which happens if the element has already been removed from the DOM by the time your callback runs. (A separate gotcha hits cross-origin images without a `Timing-Allow-Origin` header: `element` still resolves, but `renderTime` reads as `0`, so you have to fall back to `loadTime` instead — a different field going quiet, not the element.)\n\nFour lines, one footgun, done. This is not the part `web-vitals` is earning its keep on.\n\n## Where the naive version lies to you: CLS\n\nCumulative Layout Shift reports as a `layout-shift` entry every time something visible moves without the user asking it to. Each entry has a `.value` — how much of the viewport shifted, weighted by distance. The obvious thing to do is sum them:\n\n```js\nlet clsTotal = 0;\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.hadRecentInput) clsTotal += entry.value;\n  }\n  console.log(\"CLS so far:\", clsTotal);\n}).observe({ type: \"layout-shift\", buffered: true });\n```\n\n(`hadRecentInput` is true if the shift happened within 500ms of the user clicking or typing — an accordion that resizes after you click it shouldn't count against you, so the spec excludes it.)\n\nShip that on a dashboard that stays open all day, with an ad slot that occasionally reflows, and watch `clsTotal` climb to 3, then 6, then double digits by the afternoon. Nothing on the page ever visibly jumped badly — a few small shifts, hours apart. The number is technically \"correct\" by your own code, and it is nowhere close to what the real metric reports, because real CLS was never meant to be a lifetime total.\n\n**Before you scroll:** what do you think the actual rule is? A cap? A time window? A per-minute budget?\n\nIt's a *session window*. Group shifts where the gap since the last one is under 1 second, cap each group at 5 seconds total, and report whichever single window ever added up to the most:\n\n```js\nlet windows = [];\nlet current = null;\n\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (entry.hadRecentInput) continue;\n\n    const since = entry.startTime - (current?.last ?? -Infinity);\n    const span = entry.startTime - (current?.start ?? entry.startTime);\n\n    if (current && since \u003C 1000 && span \u003C 5000) {\n      current.sum += entry.value;\n      current.last = entry.startTime;\n    } else {\n      current = { start: entry.startTime, last: entry.startTime, sum: entry.value };\n      windows.push(current);\n    }\n  }\n  const cls = Math.max(0, ...windows.map((w) => w.sum));\n  console.log(\"CLS (session-windowed):\", cls);\n}).observe({ type: \"layout-shift\", buffered: true });\n```\n\nA burst of shifts from one bad ad reflow — all within a second or two of each other — stays grouped into one window and still hurts your score, correctly. But five unrelated shifts scattered across an afternoon never combine into one number, because they never do to a real visitor either: nobody experiences \"all the jank from today,\" they experience one moment where the page jumped under their thumb.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fperformanceobserver-core-web-vitals\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## INP has the same trap, with an extra twist\n\nThe naive move for Interaction to Next Paint is to listen for `event` entries and log every `duration`:\n\n```js\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(entry.name, entry.duration);\n  }\n}).observe({ type: \"event\", buffered: true, durationThreshold: 16 });\n```\n\nTap a button on a touchscreen and this fires three or four times — `pointerdown`, `pointerup`, `click`, sometimes more — for what the user experienced as *one* tap. Log each `duration` as its own interaction and you've just told your dashboard there were four slow interactions where there was one.\n\nThe fix is grouping: every `event` entry belonging to the same physical interaction shares an `interactionId`, so you group by that and take the worst duration inside each group — one number per real interaction, not one per DOM event:\n\n```js\nconst interactions = new Map();\n\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.interactionId) continue; \u002F\u002F 0 means \"not a tracked interaction\"\n    const existing = interactions.get(entry.interactionId) ?? 0;\n    interactions.set(entry.interactionId, Math.max(existing, entry.duration));\n  }\n}).observe({ type: \"event\", buffered: true, durationThreshold: 16 });\n```\n\nEven that map isn't the final INP score — the real metric doesn't just report `Math.max(...interactions.values())` either. A single freak slow tap (a GC pause at the wrong millisecond) shouldn't define the whole page's score any more than one slow HTTP request should define your p50 latency. The reference implementation tracks a high percentile across the session instead of the single worst value once there have been enough interactions to make a percentile meaningful — which is a few lines of arithmetic you do not want to be debugging for the first time during an incident.\n\n## So what is the library actually for?\n\nNot the measuring — you just watched that happen in nine lines. It's the three things nobody gets right the first time they hand-roll this: the CLS session-windowing, the INP interaction-grouping-plus-percentile, and the edge cases around bfcache restores and visibility-change timing that silently double-report or under-report a metric if you don't special-case them. That's real, fiddly logic, and `web-vitals` has had years of bug reports to get it right. If you're sending these numbers to a real-user-monitoring pipeline that someone makes decisions from, use the library for exactly that math.\n\nHere's the honest caveat, and it's narrower than it would have been a year ago: Safari was the last of the three engines to ship `largest-contentful-paint` and interaction-level `event` timing (the `interactionId` grouping above), adding both in Safari 26.2, December 2025. Firefox isn't new to this — it's had `largest-contentful-paint` since version 122 (early 2024) — but it only picked up `interactionId` around the same time as Safari, in Firefox 144. With Safari's December 2025 release, all three engines finally agreed on both entry types, which is what pushed web.dev to mark LCP and INP \"Baseline: Newly available\" at that point. `layout-shift` didn't make the trip — Cumulative Layout Shift is still Chromium-only; Firefox's tracking bug for it has sat open since 2021, and Safari 26.2 shipped the other two without it. So CLS is the one metric where neither the raw API nor `web-vitals` can produce a number for a Firefox or Safari visitor — the library's job there is still correctness on the browsers that *do* report it, not conjuring data from the one that doesn't. Feature-detect before you rely on any of this:\n\n```js\nif (PerformanceObserver.supportedEntryTypes.includes(\"layout-shift\")) {\n  \u002F\u002F measure it\n} else {\n  \u002F\u002F this visitor is on a browser that can't tell you\n}\n```\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fperformanceobserver-core-web-vitals\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nThe library isn't buying you the measurement — the browser was always going to hand you that, Chromium or nothing. It's buying you the arithmetic that's easy to get wrong exactly once, in production, where you won't notice until the dashboard looks wrong for a month. Have you ever shipped a CLS or INP number you'd now bet was quietly miscounted? I'd genuinely like to know in the comments.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Web Workers in JavaScript: The Complete Guide](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers)\n- [Your Lighthouse Score Is 100. Long Animation Frames Explain the Jank.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api)\n- [Shadow DOM Needed JavaScript to Exist. Now It Doesn't.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":96,"canonical":285,"description":286},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fperformanceobserver-core-web-vitals","The web-vitals npm package gets credit for data your browser already computes. Here's what three PerformanceObserver calls give you for free — and the one piece of math it quietly ","01a11f48-c749-7479-83cf-fc741e123165",{"id":289,"locked":18},"01a11f48-c76d-74d9-baf3-dadd0cf73d76",[291],{"id":292,"slug":95,"title":293,"_count":294},"01a11f48-c788-7276-8c70-8076af0f1bc9","PerformanceObserver and the Core Web Vitals math",{"questions":100},[296],{"locale":13,"slug":95},{"id":292,"slug":95,"title":293,"_count":298,"questionCount":100},{"questions":100},{"items":300,"meta":427},[301,312,331,352,370,391,409],{"id":94,"slug":95,"title":96,"subtitle":97,"excerpt":98,"coverUrl":99,"locale":13,"readingMinutes":100,"publishedAt":101,"viewCount":64,"likeCount":19,"commentCount":19,"author":302,"vertical":303,"topic":304,"tags":305,"_count":310,"playground":311,"hasQuiz":17,"hasPlayground":17},{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":50,"name":110},[306,307,308,309],{"slug":50,"name":51,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":86,"name":87,"color":97},{"assessments":117},{"slug":95},{"id":313,"slug":314,"title":315,"subtitle":97,"excerpt":316,"coverUrl":317,"locale":13,"readingMinutes":318,"publishedAt":319,"viewCount":320,"likeCount":19,"commentCount":19,"author":321,"vertical":322,"topic":323,"tags":324,"_count":329,"playground":330,"hasQuiz":17,"hasPlayground":17},"01a11a22-57e3-710c-8787-8b86669fb1c9","tab-leader-election-weblocks-broadcastchannel","Your Tabs Each Open Their Own WebSocket. navigator.locks Elects One.","Six open tabs means six live connections to your server, six duplicate pushes, six timers draining the battery. navigator.locks elects a single leader tab to hold the real connection — the rest just listen.","\u002Fmedia\u002Fcovers\u002Ftab-leader-election-weblocks-broadcastchannel.png",6,"2026-10-13T06:11:00.980Z",104,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":50,"name":110},[325,326,327,328],{"slug":50,"name":51,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"slug":66,"name":67,"color":97},{"assessments":117},{"slug":314},{"id":332,"slug":333,"title":334,"subtitle":97,"excerpt":335,"coverUrl":336,"locale":13,"readingMinutes":337,"publishedAt":338,"viewCount":339,"likeCount":19,"commentCount":19,"author":340,"vertical":341,"topic":342,"tags":345,"_count":350,"playground":351,"hasQuiz":17,"hasPlayground":17},"01a0ec73-2e41-711f-9f8a-b6c9bae1e054","vue-weekly-watch-vs-watcheffect","Vue watch vs watchEffect: The Complete Guide","Vue's watch() and watchEffect() look interchangeable but track dependencies, run timing, and cleanup differently. The exact rules, verified against Vue 3.5.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-watch-vs-watcheffect.png",15,"2026-10-12T06:10:54.504Z",187,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":343,"name":344},"vue","Vue",[346,347,348,349],{"slug":343,"name":344,"color":97},{"slug":50,"name":51,"color":97},{"slug":58,"name":59,"color":97},{"slug":46,"name":47,"color":97},{"assessments":117},{"slug":333},{"id":353,"slug":354,"title":355,"subtitle":97,"excerpt":356,"coverUrl":357,"locale":13,"readingMinutes":318,"publishedAt":358,"viewCount":359,"likeCount":19,"commentCount":19,"author":360,"vertical":361,"topic":362,"tags":363,"_count":368,"playground":369,"hasQuiz":17,"hasPlayground":17},"01a114fb-f147-727a-a137-53c8ce35f6bf","service-worker-stale-cache-deploy","I Shipped a Fix. My Service Worker Kept Serving the Bug.","A critical bug fix went out, and support tickets kept coming in from people who swore they'd refreshed. The code was fine. The service worker's cache-first strategy was doing its job perfectly — which was the whole problem.","\u002Fmedia\u002Fcovers\u002Fservice-worker-stale-cache-deploy.png","2026-10-12T06:10:22.830Z",329,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":50,"name":110},[364,365,366,367],{"slug":50,"name":51,"color":97},{"slug":46,"name":47,"color":97},{"slug":78,"name":79,"color":97},{"slug":66,"name":67,"color":97},{"assessments":117},{"slug":354},{"id":371,"slug":372,"title":373,"subtitle":97,"excerpt":374,"coverUrl":375,"locale":13,"readingMinutes":376,"publishedAt":377,"viewCount":378,"likeCount":19,"commentCount":19,"author":379,"vertical":380,"topic":381,"tags":384,"_count":389,"playground":390,"hasQuiz":17,"hasPlayground":17},"01a11d02-8b28-774b-9f36-fb66bece8e50","nuxt-weekly-4-migration-breaking-changes","Nuxt 4 Migration: The Breaking Changes That Actually Bite","Nuxt 3 reached end-of-life on July 31, 2026. Here's exactly what breaks moving to Nuxt 4 — directories, data fetching, TypeScript — plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-4-migration-breaking-changes.png",14,"2026-10-11T12:26:26.492Z",255,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":382,"name":383},"nuxt","Nuxt",[385,386,387,388],{"slug":382,"name":383,"color":97},{"slug":50,"name":51,"color":97},{"slug":58,"name":59,"color":97},{"slug":46,"name":47,"color":97},{"assessments":117},{"slug":372},{"id":392,"slug":393,"title":394,"subtitle":97,"excerpt":395,"coverUrl":396,"locale":13,"readingMinutes":318,"publishedAt":397,"viewCount":398,"likeCount":19,"commentCount":19,"author":399,"vertical":400,"topic":401,"tags":402,"_count":407,"playground":408,"hasQuiz":17,"hasPlayground":17},"01a10fd5-be9a-719b-8acf-e0e930c602f8","long-animation-frames-api","Your Lighthouse Score Is 100. Long Animation Frames Explain the Jank.","Lighthouse says 100 and CI is green, but a user still says the filter button feels sluggish. The Long Animation Frames API names the exact script and phase that blocked the frame — here's how to read it instead of squinting at a flame chart.","\u002Fmedia\u002Fcovers\u002Flong-animation-frames-api.png","2026-10-11T12:25:55.553Z",272,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":50,"name":110},[403,404,405,406],{"slug":50,"name":51,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":86,"name":87,"color":97},{"assessments":117},{"slug":393},{"id":410,"slug":411,"title":412,"subtitle":97,"excerpt":413,"coverUrl":414,"locale":13,"readingMinutes":80,"publishedAt":415,"viewCount":416,"likeCount":19,"commentCount":19,"author":417,"vertical":418,"topic":419,"tags":420,"_count":425,"playground":426,"hasQuiz":17,"hasPlayground":17},"01a117e0-acb2-740f-9391-db919729cf21","react-weekly-fragment-refs","React 19.3 Fragment Refs: Skip the Wrapper Div for a Ref","React 19.3 Fragment Refs let you ref a sibling group with no wrapper div. Learn the FragmentInstance API, its limits, and get a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Freact-weekly-fragment-refs.png","2026-10-10T12:12:38.989Z",281,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":74,"name":75},[421,422,423,424],{"slug":74,"name":75,"color":97},{"slug":50,"name":51,"color":97},{"slug":58,"name":59,"color":97},{"slug":46,"name":47,"color":97},{"assessments":117},{"slug":411},{"page":117,"perPage":100,"total":52,"totalPages":76},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entries \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> lastEntry \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> entries\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>entries\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>length \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\n  console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"LCP so far:\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> lastEntry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> lastEntry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>element\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"largest-contentful-paint\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> clsTotal \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entry \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>hadRecentInput\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> clsTotal \u003Cspan class=\"shj-oper\">+=\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"CLS so far:\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> clsTotal\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"layout-shift\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> windows \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> current \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entry \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>hadRecentInput\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">continue\u003C\u002Fspan>;\n\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> since \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>current\u003Cspan class=\"shj-oper\">?.\u003C\u002Fspan>last \u003Cspan class=\"shj-oper\">??\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan>\u003Cspan class=\"shj-class\">Infinity\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> span \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>current\u003Cspan class=\"shj-oper\">?.\u003C\u002Fspan>start \u003Cspan class=\"shj-oper\">??\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>current \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> since \u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1000\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> span \u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan> \u003Cspan class=\"shj-num\">5000\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      current\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>sum \u003Cspan class=\"shj-oper\">+=\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value;\n      current\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>last \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">else\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      current \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> start\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> last\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>startTime\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> sum\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n      windows\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">push\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>current\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> cls \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Math\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">max\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>windows\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">map\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>w\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> w\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>sum\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"CLS (session-windowed):\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> cls\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"layout-shift\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entry \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>name\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>duration\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"event\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> durationThreshold\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">16\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> interactions \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Map\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entry \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>interactionId\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">continue\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F 0 means \"not a tracked interaction\"\n\u003C\u002Fspan>    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> existing \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> interactions\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>interactionId\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">??\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>;\n    interactions\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>interactionId\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Math\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">max\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>existing\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>duration\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"event\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> durationThreshold\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">16\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>supportedEntryTypes\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">includes\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"layout-shift\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F measure it\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">else\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F this visitor is on a browser that can't tell you\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":435},[]]