G4.2 Guide · Web development

← Web guides

Performance and accessibility checklist.

The checklist every page we ship passes. Top ten performance failures we find, top eight accessibility failures, with fix code inline.

Length: 25 min Audience: engineering lead / founder Last updated: 2026-04-19

The bar

  • Lighthouse: Performance 95+, Accessibility 100, Best Practices 100, SEO 100. Enforced in CI on every PR that touches a page route.
  • Core Web Vitals: LCP under 2.5s, INP under 200ms, CLS under 0.1. Measured on a 3G profile, not just fiber.
  • WCAG: 2.2 AA on every page. 2.2 AAA on core flows.
  • Bundle budgets: 90 KB JS per route (gzip), 40 KB CSS per route (gzip), zero blocking render for anything above the fold.

These targets are tight. They are also regularly missed in the wild on SaaS dashboards that cost millions to build. The failures are predictable; fixing them is not rocket science.

Top ten performance failures

1. Unbounded third-party scripts

Analytics, chat widgets, session replay, sales tools. Each one adds 20 to 200 KB of JS and a couple of render-blocking requests. Audit, cull, defer.

<script src="https://third-party.example/thing.js"
  defer async></script>

Or better, load after interaction.

2. Hero image not optimized

The single largest image on the page. Ship AVIF or WebP with a correct sizes attribute, a fetchpriority="high", and dimensions set to prevent CLS.

<img src="/hero.avif" width="1280" height="720"
  fetchpriority="high" decoding="async"
  sizes="(min-width: 1024px) 1280px, 100vw"
  alt="What the product does, specifically" />

3. Web fonts blocking render

Use font-display: swap or optional. Preload the weights actually used above the fold. Serve woff2 only. Self-host the critical weights; let the rest load lazily from a CDN.

4. Full dashboard rendered client-side

Server-render the shell and the above-the-fold content. Stream the rest. Lazy-load chart libraries (they are usually 80 KB+). Use skeleton states that match final layout.

5. Route-level code splitting missing

One big bundle ships on every page. Split per route (Next.js does this for you if you let it). Dynamic-import heavy modal / table / chart components.

6. Data fetching in waterfalls

Requests should start as early as possible, ideally from the server. If the page shows three pieces of data, fetch them in parallel, not sequentially. Use React Suspense or equivalent patterns.

7. Unminified or un-gzipped bundles

Check in the browser network tab. Minified JS is typically 30% of source; gzip shrinks it another 70%. If a 1 MB bundle shows up, configuration is wrong.

8. No image lazy-loading below the fold

loading="lazy" on every image below the fold. Below-the-fold images should not block LCP.

9. Layout shift from late-loading content

Reserve space for anything that will render. Set explicit dimensions on images, videos, ad slots, dynamic content blocks. aspect-ratio is your friend.

10. Too many re-renders in dashboard state

React DevTools profiler will tell you. Common fixes: memoize expensive children, move state down the tree, use useMemo / useCallback where the dependency list is genuinely stable, virtualize long lists.

Top eight accessibility failures

1. Poor color contrast

AA requires 4.5:1 for body text, 3:1 for large text. Check with axe-core in CI, with the Contrast Analyzer in review, with the design system tokens at authoring time.

2. Missing form labels

Every input has a visible label, or an aria-label, or an aria-labelledby. Placeholder is not a label; it disappears on focus.

<label htmlFor="email">Work email</label>
<input id="email" type="email" required
  aria-describedby="email-help" />
<p id="email-help">We will not share your email.</p>

3. Keyboard traps

Every interactive element is reachable by keyboard. Modal dialogs trap focus inside the modal while open and restore focus on close. Custom menu components implement arrow-key navigation. Test by tabbing through the whole page.

4. Missing or wrong alt text

Decorative images get alt="". Informational images get a description that conveys the information. Never alt="image"; never an AI-generated stuffed-keyword description.

5. Divs used as buttons

<button> for buttons, <a> for links. If you must use a div, it needs role="button", tabindex="0", keyboard handlers for space and enter, and focus styles.

6. Missing or broken heading hierarchy

One h1 per page. Headings nest; do not skip levels for visual styling. Screen readers use headings as navigation.

7. No skip link

The first focusable element on the page should let a keyboard user skip to main content. Invisible until focused.

<a href="#main" class="skip">Skip to content</a>

8. Auto-playing media

Video that starts with sound. Animations without a prefers-reduced-motion guard. Carousels that rotate without user input. All WCAG failures, all avoidable.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

The CI pipeline

This is what we run on every PR:

  • TypeScript --noEmit for type safety.
  • ESLint with jsx-a11y rules enabled.
  • axe-core in a Playwright test suite against the pages touched.
  • Lighthouse CI against a staging deploy of the PR; regression threshold on the metrics above.
  • Bundle analyzer; comment on PR if bundle exceeds the route budget.
  • Visual regression via Playwright screenshots (optional; hard to maintain well).

What this does not catch

Automated checks catch about 30% of accessibility issues. The other 70% require a human who:

  • Actually uses a screen reader (VoiceOver, NVDA, JAWS) through the flow.
  • Navigates the whole page by keyboard only.
  • Tries to complete core tasks with motor, visual, or cognitive simulations.

Every engagement includes at least one manual accessibility audit by a named operator. If you want a full third-party audit against WCAG 2.2 AA, we contract that out and pass the cost through at no markup.

Related