10 — Optimize Performance
Optimize a module or page to meet Core Web Vitals targets and performance budgets.
Examples: Large LCP images, excessive JS bundles, layout shifts, slow interaction handlers.
Context Loading
Section titled “Context Loading”Read these files before executing the prompt:
docs/PRD/09_Cache_and_Performance.mddocs/PRD/13_Media_and_Image_Pipeline.mddocs/PRD/04_Frontend_Architecture.mdpackages/modules/src/{MODULE_PATH}/ # all files in the module to optimizeapps/web/src/ # page-level code if optimizing a pagePrompt Template
Section titled “Prompt Template”Read the following context files first:- docs/PRD/09_Cache_and_Performance.md- docs/PRD/13_Media_and_Image_Pipeline.md- docs/PRD/04_Frontend_Architecture.md- {COMPONENT_FILES — list each file in the module or page directory}
Now optimize the following for performance:
**What to Optimize:** {MODULE_NAME | PAGE_NAME}**Component Path:** packages/{PACKAGE}/src/{COMPONENT_PATH}/**Module ID:** {MODULE_ID}
**Current Lighthouse Scores:**- Performance: {CURRENT_PERF_SCORE}- Accessibility: {CURRENT_A11Y_SCORE}- SEO: {CURRENT_SEO_SCORE}
**Current Core Web Vitals (if measured):**- LCP: {CURRENT_LCP — e.g., 3.8s}- FID/INP: {CURRENT_FID_INP — e.g., 250ms}- CLS: {CURRENT_CLS — e.g., 0.25}
**Specific Issues Identified:**{LIST_EACH_ISSUE_WITH_DETAILS}Examples:- LCP image is 1.2MB, served as PNG without responsive srcset- Module bundles 180KB of JavaScript including a full carousel library- Layout shift of 0.15 caused by images loading without width/height- INP of 280ms caused by heavy click handler re-rendering the entire grid
**Target Metrics:**- Lighthouse Performance: >= 90- Lighthouse Accessibility: >= 95- Lighthouse SEO: >= 95- LCP: < 2.5s- FID: < 100ms- CLS: < 0.1- INP: < 200ms- Page total: < 500KB gzip- JS per page: < 200KB- CSS per page: < 50KB- LCP image desktop: < 500KB- LCP image mobile: < 200KB
Optimize the component following these strategies:- Use Server Components by default; only add 'use client' for actual interactivity- Use next/image for all images with proper width, height, and responsive sizes- Add `priority` prop to LCP images (above-the-fold hero images)- Use dynamic imports (`next/dynamic`) for heavy Client Components below the fold- Code-split heavy third-party libraries (carousel, lightbox, map) with dynamic imports- Lazy load below-the-fold content with `loading="lazy"` or Intersection Observer- Ensure all images have explicit width and height to prevent CLS- Use `fetchPriority="high"` for LCP images, `fetchPriority="low"` for below-fold images- Minimize client-side JavaScript — move logic to the server where possible- Use CSS for animations instead of JavaScript where feasible- Document the JS bundle contribution of this module after optimizationAcceptance Criteria
Section titled “Acceptance Criteria”- Lighthouse Performance score >= 90
- Lighthouse Accessibility score >= 95
- Lighthouse SEO score >= 95
- LCP < 2.5s (measured on simulated slow 4G)
- FID / INP < 200ms
- CLS < 0.1
- Total page size < 500KB gzip
- JS bundle contribution of this module documented (should be < 200KB per page total)
- CSS contribution < 50KB per page total
- LCP image desktop < 500KB, mobile < 200KB
- All images use
next/imagewith properwidth,height, andsizesattributes - LCP image has
priorityprop andfetchPriority="high" - Below-fold images use
loading="lazy" - Heavy Client Components use
next/dynamicwith appropriate loading fallback - No unnecessary
'use client'directives — Server Components used by default - No layout shifts from images, fonts, or dynamically loaded content
- Third-party libraries code-split via dynamic imports
-
pnpm buildsucceeds and@next/bundle-analyzershows reduced bundle size - Existing functionality and visual design preserved after optimization
- All tests still pass (
pnpm test)
Common Pitfalls
Section titled “Common Pitfalls”- Making everything a Client Component — Only components that need interactivity (event handlers, useState, useEffect) should be Client Components. Static rendering, data fetching, and layout should remain Server Components.
- Not using
next/image— Raw<img>tags miss automatic optimization (WebP/AVIF conversion, responsive srcset, lazy loading). Always use the Next.js Image component. - Loading all carousel slides eagerly — A carousel with 10 slides should only load the first 1-2 images eagerly. The rest should be lazy-loaded as the user navigates.
- Not code-splitting heavy libraries — Libraries like Swiper, Lightbox, or Google Maps should be dynamically imported so they are not included in the initial page bundle.
- Missing
widthandheighton images — Without explicit dimensions, the browser cannot reserve space for the image, causing layout shifts (CLS) when it loads. - Forgetting
priorityon LCP images — The hero image or first visible image on the page must havepriorityto trigger preloading. Without it, LCP will be slow. - Using JavaScript for animations — CSS transitions and animations are GPU-accelerated and do not block the main thread. Use
transformandopacityinstead of animatingwidth,height, ortop. - Font flash (FOIT/FOUT) — Ensure fonts are preloaded and use
font-display: swapto avoid invisible text during font loading. Configure this in the Next.js font setup. - Inline scripts blocking render — Avoid inline
<script>tags. Usenext/scriptwithstrategy="lazyOnload"for analytics and third-party scripts. - Not measuring after changes — Always re-run Lighthouse and check bundle size after optimization to verify the improvements meet targets.
Example
Section titled “Example”Filled-in prompt for M05 Hero Slider — LCP and bundle optimization:
Read the following context files first:- docs/PRD/09_Cache_and_Performance.md- docs/PRD/13_Media_and_Image_Pipeline.md- docs/PRD/04_Frontend_Architecture.md- packages/modules/src/m05-hero-slider/index.tsx- packages/modules/src/m05-hero-slider/m05-hero-slider.module.scss- packages/modules/src/m05-hero-slider/m05-hero-slider.types.ts- packages/modules/src/m05-hero-slider/m05-hero-slider.mapper.ts
Now optimize the following for performance:
**What to Optimize:** M05 Hero Slider**Component Path:** packages/modules/src/m05-hero-slider/**Module ID:** M05
**Current Lighthouse Scores:**- Performance: 62- Accessibility: 94- SEO: 92
**Current Core Web Vitals (if measured):**- LCP: 4.1s- FID/INP: 180ms- CLS: 0.22
**Specific Issues Identified:**- LCP image is the hero slide, served as a 1.8MB PNG at 2560x1440 without responsive srcset. The mobile version is the same image scaled down by CSS — no separate mobile crop.- The carousel library (Swiper) is imported statically, adding 95KB to the initial JS bundle even though the carousel is interactive and could be dynamically imported.- All 5 carousel slides load their images eagerly on page load, totaling 7.2MB of images.- CLS of 0.22 caused by the hero container not having a fixed aspect ratio — it collapses to 0px height until the first image loads, then jumps to full height.- The slide transition uses JavaScript-driven animation instead of CSS transforms.
**Target Metrics:**- Lighthouse Performance: >= 90- Lighthouse Accessibility: >= 95- Lighthouse SEO: >= 95- LCP: < 2.5s- FID: < 100ms- CLS: < 0.1- INP: < 200ms- Page total: < 500KB gzip- JS per page: < 200KB- CSS per page: < 50KB- LCP image desktop: < 500KB- LCP image mobile: < 200KB
Optimize the component following these strategies:- Use Server Components by default; only add 'use client' for actual interactivity- Use next/image for all images with proper width, height, and responsive sizes- Add `priority` prop to the first slide's LCP image only- Use dynamic imports (`next/dynamic`) for the Swiper carousel- Lazy load slide images for slides 2-5 (only the first slide should be eager)- Set a fixed aspect ratio on the hero container (16:9 desktop, 4:3 mobile) to prevent CLS- Use `fetchPriority="high"` for the first slide image- Replace JavaScript slide animation with CSS `transform: translateX()` transitions- Use separate imageDesktop and imageMobile with appropriate sizes: Desktop: sizes="100vw" with max 1920px width Mobile: sizes="100vw" with max 750px width- Document the JS bundle contribution after Swiper is dynamically imported