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
--noEmitfor type safety. - ESLint with
jsx-a11yrules 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.