Skip to content

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.


Read these files before executing the prompt:

docs/PRD/09_Cache_and_Performance.md
docs/PRD/13_Media_and_Image_Pipeline.md
docs/PRD/04_Frontend_Architecture.md
packages/modules/src/{MODULE_PATH}/ # all files in the module to optimize
apps/web/src/ # page-level code if optimizing a page

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 optimization

  • 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/image with proper width, height, and sizes attributes
  • LCP image has priority prop and fetchPriority="high"
  • Below-fold images use loading="lazy"
  • Heavy Client Components use next/dynamic with 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 build succeeds and @next/bundle-analyzer shows reduced bundle size
  • Existing functionality and visual design preserved after optimization
  • All tests still pass (pnpm test)

  1. 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.
  2. Not using next/image — Raw &lt;img&gt; tags miss automatic optimization (WebP/AVIF conversion, responsive srcset, lazy loading). Always use the Next.js Image component.
  3. 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.
  4. 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.
  5. Missing width and height on images — Without explicit dimensions, the browser cannot reserve space for the image, causing layout shifts (CLS) when it loads.
  6. Forgetting priority on LCP images — The hero image or first visible image on the page must have priority to trigger preloading. Without it, LCP will be slow.
  7. Using JavaScript for animations — CSS transitions and animations are GPU-accelerated and do not block the main thread. Use transform and opacity instead of animating width, height, or top.
  8. Font flash (FOIT/FOUT) — Ensure fonts are preloaded and use font-display: swap to avoid invisible text during font loading. Configure this in the Next.js font setup.
  9. Inline scripts blocking render — Avoid inline &lt;script&gt; tags. Use next/script with strategy="lazyOnload" for analytics and third-party scripts.
  10. Not measuring after changes — Always re-run Lighthouse and check bundle size after optimization to verify the improvements meet targets.

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