[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6D1YJ0WS5CEYA02PB8ANQSC":3,"skill-same:01M6D1YJ0WS5CEYA02PB8ANQSC":76},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":48,"linkOnly":49,"markdownContent":50,"markdownBody":51,"frontMatter":52,"refName":53,"commitSha":54,"skillDirectory":55,"forksCount":56,"repositoryHtmlUrl":57,"repositoryHomepageUrl":58,"ownerHtmlUrl":59,"ownerBlogUrl":58,"ownerDescription":60,"updatedAt":61,"install":62,"resources":75},"01M6D1YJ0WS5CEYA02PB8ANQSC","frontend-performance",null,"Making a web frontend fast: Core Web Vitals and budgets, what loads first, images and fonts, JavaScript weight and code splitting, rendering strategy and hydration, INP and long tasks, layout shift, caching and a CDN, long lists, and measuring in the lab and the field. Read before building a page that must load fast, and when one is slow.","enowdev\u002Fenowxcli","enowdev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179420349?v=4","01M6D1MVX2KC0X3Z9NESE7P22K","Enow Dev.","01M6D1MW1FMZGBDKWAJY4GQX81",67,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Ffrontend-performance\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"performance","frontend performance","frontend","first","layout","caching","images","strategy","read","before building","page","javascript","core web vitals","fast","cdn","code","core","tasks","web","2026-11-10 10:12:30",false,"---\nname: frontend-performance\ndescription: \"Making a web frontend fast: Core Web Vitals and budgets, what loads first, images and fonts, JavaScript weight and code splitting, rendering strategy and hydration, INP and long tasks, layout shift, caching and a CDN, long lists, and measuring in the lab and the field. Read before building a page that must load fast, and when one is slow.\"\n---\n\n# A fast frontend\n\nThe generated page is fast on the laptop that built it and slow\neverywhere else: a 3000px hero image lazy-loaded, four font weights from a\nthird-party CDN, 900 kB of JavaScript for a page of text, a chart library\nin the first bundle, 5,000 rows rendered at once, a banner that pushes the\npage down as it arrives, and nothing measured. This is how to budget,\nbuild and measure for a mid-range phone on a mobile network, which is\nwhere most visits come from. Animation cost is `motion-performance`; the\nwhole frontend is `frontend`.\n\n## 1. Budgets\n\nCore Web Vitals, at the 75th percentile of real visits, on mobile:\n\n| Metric | Good | Poor | Measures |\n|---|---|---|---|\n| LCP, Largest Contentful Paint | 2.5s or less | over 4s | when the main content appears |\n| INP, Interaction to Next Paint | 200ms or less | over 500ms | how fast taps, clicks and keys get a response |\n| CLS, Cumulative Layout Shift | 0.1 or less | over 0.25 | how much the page jumps |\n\n- Behind them: time to first byte 0.8s or less, first contentful paint\n  1.8s or less.\n- JavaScript loaded before the page is usable, compressed: a content page\n  under about 150 kB, an application shell under about 250 kB. Each route\n  then adds only its own code.\n- The LCP image usually fits in 100 to 200 kB as AVIF or WebP at its shown\n  size; fonts in two to four files of 15 to 40 kB each once subset.\n- Written down and checked in CI (section 10), or the budget is gone by the\n  third feature.\n\n## 2. What loads first\n\n- A fast first byte: HTML from a CDN (static or cached), rendered near the\n  data when it is dynamic, and no redirect chain (`http` to `https` to\n  `www` to `\u002Fen` is three round trips).\n- Find the LCP element (the Performance panel, or the web-vitals\n  attribution build) and make it early. An image: an `\u003Cimg>` in the HTML\n  (not a CSS background, not inserted by a script), `fetchpriority=\"high\"`,\n  never `loading=\"lazy\"`, preloaded only when discovered late. Text: its\n  font must not hold it back (section 4).\n- CSS in the `head`, small; scripts `defer` or `type=\"module\"`; no\n  synchronous third-party script in the `head`.\n- `preconnect` to the one to three other origins needed at once (an image\n  CDN, the API); more costs more than it saves. Fonts need `crossorigin`\n  on their preconnect and preload; images do not.\n\n```html\n\u003Clink rel=\"preconnect\" href=\"https:\u002F\u002Fimg.example-cdn.net\">\n\u003Clink rel=\"preload\" href=\"\u002Ffonts\u002Fbody-var.woff2\" as=\"font\" type=\"font\u002Fwoff2\" crossorigin>\n\u003Cimg src=\"\u002Fimg\u002Fhero-1200.avif\"\n     srcset=\"\u002Fimg\u002Fhero-800.avif 800w, \u002Fimg\u002Fhero-1200.avif 1200w, \u002Fimg\u002Fhero-2000.avif 2000w\"\n     sizes=\"(min-width: 1024px) 50vw, 100vw\"\n     width=\"1200\" height=\"800\" fetchpriority=\"high\" alt=\"[What the photo shows]\">\n```\n\n- Pages people are likely to open next can load before the click: the\n  framework's link prefetching, or Speculation Rules\n  (`\u003Cscript type=\"speculationrules\">`) in Chromium browsers.\n- Keep pages eligible for the back\u002Fforward cache, which makes Back\n  instant: no `unload` listeners, no `Cache-Control: no-store` on HTML\n  without a reason. DevTools, Application, Back\u002Fforward cache tests it.\n\n## 3. Images\n\n- AVIF, with WebP as the fallback (`\u003Cpicture>`, or an image CDN choosing by\n  the `Accept` header); SVG for logos and icons; PNG only where lossless\n  matters and WebP will not do.\n- `srcset` with widths and a `sizes` matching the layout, so a phone never\n  downloads the desktop file; at most twice the displayed width.\n- `width` and `height` on every `img` (or `aspect-ratio`), so its space is\n  kept before it loads.\n- `loading=\"lazy\"` and `decoding=\"async\"` below the fold, never on the LCP\n  image.\n- Let the framework do it: `next\u002Fimage` (with `sizes` whenever it uses\n  `fill`), Astro's `\u003CImage>` and `\u003CPicture>`, Nuxt Image, `@unpic` for image\n  CDNs, `vite-imagetools` in Vite. Quality around 75 is a good start.\n- Video: a poster, `preload=\"none\"` or `\"metadata\"`, no autoplaying hero\n  video on phones; decorative loops `muted playsinline`, short and small.\n\n## 4. Fonts\n\n- `woff2` only, self-hosted (next\u002Ffont, Fontsource packages, or files in\n  the repository), subset to the scripts the pages use (`unicode-range`,\n  `pyftsubset`); one variable font rather than four static weights; two\n  families at most (`ui`).\n- Preload only the one or two files used above the fold.\n- `font-display: swap` shows text at once in the fallback; `optional` uses\n  the web font only if it arrives within about 100ms, and never shifts.\n- Match the fallback's metrics so the swap does not move text. next\u002Ffont\n  does it by itself; elsewhere Fontaine or Capsize compute the overrides\n  for your font:\n\n```css\n@font-face {\n  font-family: \"Body Fallback\";\n  src: local(\"Arial\");\n  size-adjust: 104%;       \u002F* computed for the real font, never copied *\u002F\n  ascent-override: 92%;\n  descent-override: 24%;\n  line-gap-override: 0%;\n}\nbody { font-family: \"Body\", \"Body Fallback\", sans-serif; }\n```\n\n## 5. JavaScript weight\n\n- Split by route: automatic in Next, SvelteKit, Nuxt, Astro and TanStack\n  Router; `lazy` in React Router; `React.lazy` elsewhere.\n- Load heavy widgets when needed, with `import()`: rich text and code\n  editors, charts, maps, PDF viewers, emoji pickers, syntax highlighters;\n  browser-only ones with `next\u002Fdynamic` and `ssr: false`, in a client component.\n- Imports that tree-shake: named imports from ES modules, icons one by\n  one, `date-fns` functions rather than a whole date library, no\n  `import * as`. `Intl` formats dates and numbers at no bundle cost.\n- Find the weight: `npx vite-bundle-visualizer`, `rollup-plugin-visualizer`,\n  `@next\u002Fbundle-analyzer`, or `npx source-map-explorer dist\u002Fassets\u002F*.js`.\n  Remove unused packages (`npx knip`) and duplicate versions\n  (`pnpm dedupe`).\n- Compile for the browsers you support (`build.target`, `browserslist`);\n  no polyfills for what they all have.\n- Third-party scripts are the usual culprit: each tag manager, chat\n  widget, A\u002FB tool and pixel runs on the main thread. Keep few, load them\n  after the page is interactive or on first use (`next\u002Fscript` with\n  `strategy=\"lazyOnload\"`), and put a facade in front of heavy embeds (a\n  thumbnail that loads the video player on click, a static map image).\n\n## 6. Rendering and hydration\n\n- Content pages: static or incrementally regenerated HTML from a CDN.\n  Server-rendered pages: stream the shell at once, the slow parts behind\n  `Suspense`.\n- Hydration runs the page's components again in the browser, costing in\n  proportion to the JavaScript and data shipped. Ship less: React Server\n  Components keep markdown parsing, syntax highlighting and formatting on\n  the server; Astro islands hydrate only the interactive parts\n  (`client:visible`, `client:idle`).\n- Pass client components only the fields they use: a whole record\n  serialised into the page for one name is sent twice, as markup and as\n  data.\n- Public content rendered only in the browser has the slowest LCP of all:\n  nothing shows until the bundle has loaded, run and fetched.\n- Long pages: `content-visibility: auto` with\n  `contain-intrinsic-size: auto 600px` on sections below the fold skips\n  their rendering until they come near.\n\n## 7. INP: answer within a frame or two\n\n- A long task is 50ms or more of main-thread work. An interaction waits\n  for the task ahead of it, runs its handlers, then waits for the next\n  paint; INP is close to the slowest of these on the page.\n- Handlers do the visible part first (the button pressed, the item\n  checked) and defer the rest (analytics, recalculation) until after the\n  paint. Long work runs in pieces, yielding between them:\n\n```ts\n\u002F** Lets the browser paint and handle input before the next piece of work. *\u002F\nfunction yieldToMain(): Promise\u003Cvoid> {\n  const scheduler = (globalThis as { scheduler?: { yield?: () => Promise\u003Cvoid> } }).scheduler;\n  return scheduler?.yield ? scheduler.yield() : new Promise((resolve) => setTimeout(resolve, 0));\n}\n\nlet deadline = performance.now() + 40;\nfor (const row of rows) {\n  handle(row);\n  if (performance.now() > deadline) {\n    await yieldToMain();\n    deadline = performance.now() + 40;\n  }\n}\n```\n\n- React: `useTransition` for an update that renders a lot (a heavy tab, a\n  large list filtered), `useDeferredValue` for a slow view that follows\n  fast input, so typing stays instant.\n- Debounce work triggered by typing (200 to 300ms); observers\n  (`IntersectionObserver`, `ResizeObserver`) instead of scroll and resize\n  handlers.\n- Virtualise lists past about 200 rows, sooner when rows are heavy:\n  TanStack Virtual (`useVirtualizer({ count, getScrollElement,\n  estimateSize: () => 44, overscan: 8 })`) or react-virtuoso. Rows outside\n  the window are not in the DOM, so find-in-page misses them: give the\n  list its own search.\n- Keep the DOM small: thousands of nodes make every style and layout pass\n  slower. Read sizes (`getBoundingClientRect`, `offsetHeight`) together,\n  then write styles together, never alternating in a loop.\n- Heavy computation (parsing a large file, image work, diffing) goes to a\n  Web Worker; Comlink makes it a function call.\n\n## 8. CLS: nothing jumps\n\n- Reserve space for everything that loads: images and video (`width` and\n  `height`), embeds and ads (`aspect-ratio` or `min-height` on the slot),\n  late banners (overlaid, or their space kept from the start).\n- Never insert content above what the reader is looking at: new items\n  arrive below, or behind a \"Show 3 new\" button.\n- Fallback font metrics (section 4); skeletons the size of the content.\n- Animate `transform` and `opacity`, never `top`, `height` or `margin`\n  (`motion-performance`).\n- Shifts within 500ms after a tap or key press do not count: an accordion\n  opening on click is fine.\n\n## 9. Caching and delivery\n\n- Built assets with hashed names, cached for a year:\n  `Cache-Control: public, max-age=31536000, immutable`.\n- HTML revalidated on each visit (`no-cache` with an `ETag`), or cached at\n  the CDN briefly with `stale-while-revalidate`. Long-cached HTML points at\n  files a later deploy removed (`frontend-errors`).\n- A CDN in front, Brotli for text (pre-compressed at build), HTTP\u002F2 or 3.\n- A service worker only when offline use or installing matters (Workbox,\n  `vite-plugin-pwa`, Serwist): it is a cache that serves old code when its\n  update flow is wrong.\n\n```nginx\n# An SPA on nginx\nlocation \u002Fassets\u002F { add_header Cache-Control \"public, max-age=31536000, immutable\"; }\nlocation \u002F {\n  add_header Cache-Control \"no-cache\";\n  try_files $uri \u002Findex.html;\n}\n```\n\n## 10. Measuring\n\n- Lab, while building: Lighthouse in DevTools or `npx lighthouse \u003Curl>\n  --view` (mobile and throttled by default); the Performance panel with\n  the CPU slowed 4x to 6x and the network at Slow 4G, whose live metrics\n  show LCP, INP and CLS as you use the page, with long tasks and layout\n  shifts on their tracks.\n- Field, what users get: PageSpeed Insights and the Search Console Core\n  Web Vitals report (Chrome UX Report data, 28 days, p75), and your own\n  numbers from the `web-vitals` library:\n\n```ts\nimport { onCLS, onINP, onLCP } from \"web-vitals\u002Fattribution\";\nimport type { Metric } from \"web-vitals\";\n\nconst send = (metric: Metric) => navigator.sendBeacon(\"\u002Fapi\u002Fvitals\", JSON.stringify(metric));\nonCLS(send);\nonINP(send);\nonLCP(send);\n```\n\n- In CI: `size-limit` (a `.size-limit.json` like this) fails the build over\n  budget; Lighthouse CI (`lhci autorun`) asserts on the key pages.\n\n```json\n[{ \"path\": \"dist\u002Fassets\u002Findex-*.js\", \"limit\": \"250 kB\", \"gzip\": true }]\n```\n\n- Now and then, a real mid-range Android phone: the laptop is not the\n  audience.\n\n## Check it\n\n- `pnpm build`: read each chunk's size; open the bundle analyser for\n  anything unexpected.\n- Lighthouse (mobile) on the production build of the key pages: LCP, CLS\n  and total blocking time within budget, and the LCP element is the one\n  you meant.\n- The Performance panel, throttled: tap the main controls, no task over\n  200ms; scroll the long lists, no stutter.\n- `preview` with `motion: true` reports long frames and layout shift.\n\n## Avoid\n\nA lazy-loaded or CSS-background LCP image; images with no size; the\ndesktop file sent to phones; fonts from a third-party CDN, four static\nweights, no fallback metrics; a chart, editor or map in the first bundle;\n`import *` from big libraries; tag managers loaded first; public content\nrendered only in the browser; thousands of rows in the DOM; content\ninserted above the reader; long-cached HTML; budgets nobody checks.\n","# A fast frontend\n\nThe generated page is fast on the laptop that built it and slow\neverywhere else: a 3000px hero image lazy-loaded, four font weights from a\nthird-party CDN, 900 kB of JavaScript for a page of text, a chart library\nin the first bundle, 5,000 rows rendered at once, a banner that pushes the\npage down as it arrives, and nothing measured. This is how to budget,\nbuild and measure for a mid-range phone on a mobile network, which is\nwhere most visits come from. Animation cost is `motion-performance`; the\nwhole frontend is `frontend`.\n\n## 1. Budgets\n\nCore Web Vitals, at the 75th percentile of real visits, on mobile:\n\n| Metric | Good | Poor | Measures |\n|---|---|---|---|\n| LCP, Largest Contentful Paint | 2.5s or less | over 4s | when the main content appears |\n| INP, Interaction to Next Paint | 200ms or less | over 500ms | how fast taps, clicks and keys get a response |\n| CLS, Cumulative Layout Shift | 0.1 or less | over 0.25 | how much the page jumps |\n\n- Behind them: time to first byte 0.8s or less, first contentful paint\n  1.8s or less.\n- JavaScript loaded before the page is usable, compressed: a content page\n  under about 150 kB, an application shell under about 250 kB. Each route\n  then adds only its own code.\n- The LCP image usually fits in 100 to 200 kB as AVIF or WebP at its shown\n  size; fonts in two to four files of 15 to 40 kB each once subset.\n- Written down and checked in CI (section 10), or the budget is gone by the\n  third feature.\n\n## 2. What loads first\n\n- A fast first byte: HTML from a CDN (static or cached), rendered near the\n  data when it is dynamic, and no redirect chain (`http` to `https` to\n  `www` to `\u002Fen` is three round trips).\n- Find the LCP element (the Performance panel, or the web-vitals\n  attribution build) and make it early. An image: an `\u003Cimg>` in the HTML\n  (not a CSS background, not inserted by a script), `fetchpriority=\"high\"`,\n  never `loading=\"lazy\"`, preloaded only when discovered late. Text: its\n  font must not hold it back (section 4).\n- CSS in the `head`, small; scripts `defer` or `type=\"module\"`; no\n  synchronous third-party script in the `head`.\n- `preconnect` to the one to three other origins needed at once (an image\n  CDN, the API); more costs more than it saves. Fonts need `crossorigin`\n  on their preconnect and preload; images do not.\n\n```html\n\u003Clink rel=\"preconnect\" href=\"https:\u002F\u002Fimg.example-cdn.net\">\n\u003Clink rel=\"preload\" href=\"\u002Ffonts\u002Fbody-var.woff2\" as=\"font\" type=\"font\u002Fwoff2\" crossorigin>\n\u003Cimg src=\"\u002Fimg\u002Fhero-1200.avif\"\n     srcset=\"\u002Fimg\u002Fhero-800.avif 800w, \u002Fimg\u002Fhero-1200.avif 1200w, \u002Fimg\u002Fhero-2000.avif 2000w\"\n     sizes=\"(min-width: 1024px) 50vw, 100vw\"\n     width=\"1200\" height=\"800\" fetchpriority=\"high\" alt=\"[What the photo shows]\">\n```\n\n- Pages people are likely to open next can load before the click: the\n  framework's link prefetching, or Speculation Rules\n  (`\u003Cscript type=\"speculationrules\">`) in Chromium browsers.\n- Keep pages eligible for the back\u002Fforward cache, which makes Back\n  instant: no `unload` listeners, no `Cache-Control: no-store` on HTML\n  without a reason. DevTools, Application, Back\u002Fforward cache tests it.\n\n## 3. Images\n\n- AVIF, with WebP as the fallback (`\u003Cpicture>`, or an image CDN choosing by\n  the `Accept` header); SVG for logos and icons; PNG only where lossless\n  matters and WebP will not do.\n- `srcset` with widths and a `sizes` matching the layout, so a phone never\n  downloads the desktop file; at most twice the displayed width.\n- `width` and `height` on every `img` (or `aspect-ratio`), so its space is\n  kept before it loads.\n- `loading=\"lazy\"` and `decoding=\"async\"` below the fold, never on the LCP\n  image.\n- Let the framework do it: `next\u002Fimage` (with `sizes` whenever it uses\n  `fill`), Astro's `\u003CImage>` and `\u003CPicture>`, Nuxt Image, `@unpic` for image\n  CDNs, `vite-imagetools` in Vite. Quality around 75 is a good start.\n- Video: a poster, `preload=\"none\"` or `\"metadata\"`, no autoplaying hero\n  video on phones; decorative loops `muted playsinline`, short and small.\n\n## 4. Fonts\n\n- `woff2` only, self-hosted (next\u002Ffont, Fontsource packages, or files in\n  the repository), subset to the scripts the pages use (`unicode-range`,\n  `pyftsubset`); one variable font rather than four static weights; two\n  families at most (`ui`).\n- Preload only the one or two files used above the fold.\n- `font-display: swap` shows text at once in the fallback; `optional` uses\n  the web font only if it arrives within about 100ms, and never shifts.\n- Match the fallback's metrics so the swap does not move text. next\u002Ffont\n  does it by itself; elsewhere Fontaine or Capsize compute the overrides\n  for your font:\n\n```css\n@font-face {\n  font-family: \"Body Fallback\";\n  src: local(\"Arial\");\n  size-adjust: 104%;       \u002F* computed for the real font, never copied *\u002F\n  ascent-override: 92%;\n  descent-override: 24%;\n  line-gap-override: 0%;\n}\nbody { font-family: \"Body\", \"Body Fallback\", sans-serif; }\n```\n\n## 5. JavaScript weight\n\n- Split by route: automatic in Next, SvelteKit, Nuxt, Astro and TanStack\n  Router; `lazy` in React Router; `React.lazy` elsewhere.\n- Load heavy widgets when needed, with `import()`: rich text and code\n  editors, charts, maps, PDF viewers, emoji pickers, syntax highlighters;\n  browser-only ones with `next\u002Fdynamic` and `ssr: false`, in a client component.\n- Imports that tree-shake: named imports from ES modules, icons one by\n  one, `date-fns` functions rather than a whole date library, no\n  `import * as`. `Intl` formats dates and numbers at no bundle cost.\n- Find the weight: `npx vite-bundle-visualizer`, `rollup-plugin-visualizer`,\n  `@next\u002Fbundle-analyzer`, or `npx source-map-explorer dist\u002Fassets\u002F*.js`.\n  Remove unused packages (`npx knip`) and duplicate versions\n  (`pnpm dedupe`).\n- Compile for the browsers you support (`build.target`, `browserslist`);\n  no polyfills for what they all have.\n- Third-party scripts are the usual culprit: each tag manager, chat\n  widget, A\u002FB tool and pixel runs on the main thread. Keep few, load them\n  after the page is interactive or on first use (`next\u002Fscript` with\n  `strategy=\"lazyOnload\"`), and put a facade in front of heavy embeds (a\n  thumbnail that loads the video player on click, a static map image).\n\n## 6. Rendering and hydration\n\n- Content pages: static or incrementally regenerated HTML from a CDN.\n  Server-rendered pages: stream the shell at once, the slow parts behind\n  `Suspense`.\n- Hydration runs the page's components again in the browser, costing in\n  proportion to the JavaScript and data shipped. Ship less: React Server\n  Components keep markdown parsing, syntax highlighting and formatting on\n  the server; Astro islands hydrate only the interactive parts\n  (`client:visible`, `client:idle`).\n- Pass client components only the fields they use: a whole record\n  serialised into the page for one name is sent twice, as markup and as\n  data.\n- Public content rendered only in the browser has the slowest LCP of all:\n  nothing shows until the bundle has loaded, run and fetched.\n- Long pages: `content-visibility: auto` with\n  `contain-intrinsic-size: auto 600px` on sections below the fold skips\n  their rendering until they come near.\n\n## 7. INP: answer within a frame or two\n\n- A long task is 50ms or more of main-thread work. An interaction waits\n  for the task ahead of it, runs its handlers, then waits for the next\n  paint; INP is close to the slowest of these on the page.\n- Handlers do the visible part first (the button pressed, the item\n  checked) and defer the rest (analytics, recalculation) until after the\n  paint. Long work runs in pieces, yielding between them:\n\n```ts\n\u002F** Lets the browser paint and handle input before the next piece of work. *\u002F\nfunction yieldToMain(): Promise\u003Cvoid> {\n  const scheduler = (globalThis as { scheduler?: { yield?: () => Promise\u003Cvoid> } }).scheduler;\n  return scheduler?.yield ? scheduler.yield() : new Promise((resolve) => setTimeout(resolve, 0));\n}\n\nlet deadline = performance.now() + 40;\nfor (const row of rows) {\n  handle(row);\n  if (performance.now() > deadline) {\n    await yieldToMain();\n    deadline = performance.now() + 40;\n  }\n}\n```\n\n- React: `useTransition` for an update that renders a lot (a heavy tab, a\n  large list filtered), `useDeferredValue` for a slow view that follows\n  fast input, so typing stays instant.\n- Debounce work triggered by typing (200 to 300ms); observers\n  (`IntersectionObserver`, `ResizeObserver`) instead of scroll and resize\n  handlers.\n- Virtualise lists past about 200 rows, sooner when rows are heavy:\n  TanStack Virtual (`useVirtualizer({ count, getScrollElement,\n  estimateSize: () => 44, overscan: 8 })`) or react-virtuoso. Rows outside\n  the window are not in the DOM, so find-in-page misses them: give the\n  list its own search.\n- Keep the DOM small: thousands of nodes make every style and layout pass\n  slower. Read sizes (`getBoundingClientRect`, `offsetHeight`) together,\n  then write styles together, never alternating in a loop.\n- Heavy computation (parsing a large file, image work, diffing) goes to a\n  Web Worker; Comlink makes it a function call.\n\n## 8. CLS: nothing jumps\n\n- Reserve space for everything that loads: images and video (`width` and\n  `height`), embeds and ads (`aspect-ratio` or `min-height` on the slot),\n  late banners (overlaid, or their space kept from the start).\n- Never insert content above what the reader is looking at: new items\n  arrive below, or behind a \"Show 3 new\" button.\n- Fallback font metrics (section 4); skeletons the size of the content.\n- Animate `transform` and `opacity`, never `top`, `height` or `margin`\n  (`motion-performance`).\n- Shifts within 500ms after a tap or key press do not count: an accordion\n  opening on click is fine.\n\n## 9. Caching and delivery\n\n- Built assets with hashed names, cached for a year:\n  `Cache-Control: public, max-age=31536000, immutable`.\n- HTML revalidated on each visit (`no-cache` with an `ETag`), or cached at\n  the CDN briefly with `stale-while-revalidate`. Long-cached HTML points at\n  files a later deploy removed (`frontend-errors`).\n- A CDN in front, Brotli for text (pre-compressed at build), HTTP\u002F2 or 3.\n- A service worker only when offline use or installing matters (Workbox,\n  `vite-plugin-pwa`, Serwist): it is a cache that serves old code when its\n  update flow is wrong.\n\n```nginx\n# An SPA on nginx\nlocation \u002Fassets\u002F { add_header Cache-Control \"public, max-age=31536000, immutable\"; }\nlocation \u002F {\n  add_header Cache-Control \"no-cache\";\n  try_files $uri \u002Findex.html;\n}\n```\n\n## 10. Measuring\n\n- Lab, while building: Lighthouse in DevTools or `npx lighthouse \u003Curl>\n  --view` (mobile and throttled by default); the Performance panel with\n  the CPU slowed 4x to 6x and the network at Slow 4G, whose live metrics\n  show LCP, INP and CLS as you use the page, with long tasks and layout\n  shifts on their tracks.\n- Field, what users get: PageSpeed Insights and the Search Console Core\n  Web Vitals report (Chrome UX Report data, 28 days, p75), and your own\n  numbers from the `web-vitals` library:\n\n```ts\nimport { onCLS, onINP, onLCP } from \"web-vitals\u002Fattribution\";\nimport type { Metric } from \"web-vitals\";\n\nconst send = (metric: Metric) => navigator.sendBeacon(\"\u002Fapi\u002Fvitals\", JSON.stringify(metric));\nonCLS(send);\nonINP(send);\nonLCP(send);\n```\n\n- In CI: `size-limit` (a `.size-limit.json` like this) fails the build over\n  budget; Lighthouse CI (`lhci autorun`) asserts on the key pages.\n\n```json\n[{ \"path\": \"dist\u002Fassets\u002Findex-*.js\", \"limit\": \"250 kB\", \"gzip\": true }]\n```\n\n- Now and then, a real mid-range Android phone: the laptop is not the\n  audience.\n\n## Check it\n\n- `pnpm build`: read each chunk's size; open the bundle analyser for\n  anything unexpected.\n- Lighthouse (mobile) on the production build of the key pages: LCP, CLS\n  and total blocking time within budget, and the LCP element is the one\n  you meant.\n- The Performance panel, throttled: tap the main controls, no task over\n  200ms; scroll the long lists, no stutter.\n- `preview` with `motion: true` reports long frames and layout shift.\n\n## Avoid\n\nA lazy-loaded or CSS-background LCP image; images with no size; the\ndesktop file sent to phones; fonts from a third-party CDN, four static\nweights, no fallback metrics; a chart, editor or map in the first bundle;\n`import *` from big libraries; tag managers loaded first; public content\nrendered only in the browser; thousands of rows in the DOM; content\ninserted above the reader; long-cached HTML; budgets nobody checks.\n",{"name":5,"description":7},"main","38293fe1b096e6bf3646c7344c38786ed688f464","crates\u002Fenowx-core\u002Fskills\u002Ffrontend-performance",21,"https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli","https:\u002F\u002Fenowx.ai","https:\u002F\u002Fgithub.com\u002Fenowdev","Pretending to be a Developer","2026-10-06 02:05:35",{"source":8,"skillName":5,"commands":63,"prompt":73,"zipAvailable":74},[64,67,70],{"tool":65,"command":66},"npx","npx skills add enowdev\u002Fenowxcli --skill frontend-performance",{"tool":68,"command":69},"bunx","bunx skills add enowdev\u002Fenowxcli --skill frontend-performance",{"tool":71,"command":72},"pnpm","pnpm dlx skills add enowdev\u002Fenowxcli --skill frontend-performance","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Ffrontend-performance\u002FSKILL.md",true,[],[77,91,113,127,151,173],{"skillNo":78,"name":79,"nameZh":6,"description":80,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":81,"categories":82,"keywords":86,"publishedAt":48},"01M6D2HQYSQD9N7MTNDT4A7CBM","demo-skill","Project-scope demo skill for tests.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fworkspace\u002F.agents\u002Fskills\u002Fdemo-skill\u002FSKILL.md",[83],{"categoryNo":84,"categoryName":85,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[87,88,89,90,37],"demo skill","demo","tests","project",{"skillNo":92,"name":93,"nameZh":6,"description":94,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":95,"categories":96,"keywords":103,"publishedAt":48},"01M6D2HM0GCPM0TM2HPG9HHR2R","writing","Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fwriting\u002FSKILL.md",[97,100],{"categoryNo":98,"categoryName":99,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":101,"categoryName":102,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000036","内容创作者",[93,104,105,106,107,108,37,39,109,110,111,112],"text","emails","empty","drop","document","voice","notifications","documentation","translate",{"skillNo":114,"name":115,"nameZh":6,"description":116,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":117,"categories":118,"keywords":122,"publishedAt":48},"01M6D2HG07YS49NM196Q0R3AZ8","ui","Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui\u002FSKILL.md",[119],{"categoryNo":120,"categoryName":121,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000032","设计师",[115,123,124,125,126,39,33,37,38],"icons","components","SET","responsive",{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":137,"publishedAt":48},"01M6D2HB59HZHWG1RYZDV4SJ91","ui-themes","Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-themes\u002FSKILL.md",[133,136],{"categoryNo":134,"categoryName":135,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[115,138,139,140,141,142,143,144,145,37,38,146,39,44,147,148,123,149,150],"ui themes","switching","photos","tokens","drawing","focus","maps","Art","flash","Charts","content","screenshots","logo",{"skillNo":152,"name":153,"nameZh":6,"description":154,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":155,"categories":156,"keywords":160,"publishedAt":48},"01M6D2H5R5KRAE524C8QX0T7Q0","ui-structure","Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-structure\u002FSKILL.md",[157,158,159],{"categoryNo":98,"categoryName":99,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},{"categoryNo":101,"categoryName":102,"taxonomyType":26,"taxonomyTypeDesc":27},[161,115,162,124,163,164,44,165,166,167,33,141,168,169,170,37,38,39,171,172],"structure","ui structure","pricing","dashboard","Blueprint","article","anatomy","styling","primitives","list","why","how",{"skillNo":174,"name":175,"nameZh":6,"description":176,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":177,"categories":178,"keywords":184,"publishedAt":48},"01M6D2H0ZZBJG77MBGPZHA6MWD","ui-stack-vue","Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-vue\u002FSKILL.md",[179,182,183],{"categoryNo":180,"categoryName":181,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":120,"categoryName":121,"taxonomyType":26,"taxonomyTypeDesc":27},[115,185,186,187,188,189,190,191,33,90,192,193,37,194,195,196,197,198,199,200],"vue","stack","ui stack vue","forms","pages","watch","runtime","transitions","fetch","pinia","nuxt","template","server","config","directory","testing"]