SaaS performance checklist
The reasoning behind every fix. Why this is the bar.
The 18 failures we see on most audits and the fix code for each. Performance and accessibility, ranked by frequency in our field work.
Template - copy freely
Single biggest LCP offender. JPEG at original dimensions, no srcset, no priority.
Fix:
<img
src="/hero-800.webp"
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1600.webp 1600w"
sizes="(max-width: 768px) 100vw, 800px"
width="800" height="450"
alt="..."
fetchpriority="high"
loading="eager"
/>
Fix: preload the two hero fonts, use font-display: swap, self-host.
<link rel="preload" href="fonts/geist-variable.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: "Geist";
src: url("fonts/geist-variable.woff2") format("woff2-variations");
font-display: swap;
font-weight: 100 900;
}
Fix: enable brotli at the edge. For Vite / Next, confirm build output uses content hashes and is served with long cache TTL. Audit imports - the classic offender is importing entire libraries (lodash, date-fns) when you only use two functions.
Fix: Load analytics, chat widgets, A/B tools with async defer. Use <script type="module"> where possible. For multiple third parties, consider Partytown or server-side alternatives.
Fix: reserve space with explicit width and height on all images, iframes, and ad slots. Use CSS aspect-ratio.
Fix: ship production builds. Confirm source maps are not served to end users (robots-excluded or behind auth).
Fix: for Next / Remix / SvelteKit, use server components or islands architecture. Hydrate only interactive pieces. Avoid hydrating marketing pages as full SPA.
Fix: profile with Chrome DevTools Performance panel. Split long tasks with scheduler.yield() or requestIdleCallback. Move CPU-heavy work to Web Workers.
Fix: host on a CDN with HTTP/3 (Cloudflare, Fastly, Vercel, Netlify). Self-hosted origins should enable HTTP/2 at minimum.
Fix: strip <link rel="prefetch"> that load non-critical resources on mobile. Measure before enabling global prefetch in Next.js.
Decorative images need alt=""; content images need descriptive alt; interactive images need functional alt (“close menu”, not “x”).
Fix: minimum 4.5:1 for normal text, 3:1 for large. Our --text-muted used to be a contrast fail - increased to #C9CFDB.
Fix: use <button>. If you must style differently, reset with appearance: none. Keyboard access and screen reader support come free.
Fix: every input has a <label for=”id”> or is wrapped in a label. aria-label is fallback, not default.
Fix: never set outline: none without a replacement. We use a 2px lime ring on focus-visible.
:focus-visible {
outline: 2px solid var(--lime);
outline-offset: 2px;
border-radius: 0.25rem;
}
Fix: one H1 per page. H2 for sections, H3 for subsections. No jumping H2 to H4 for styling.
Fix: use <details><summary> for simple disclosure. For complex menus, implement proper WAI-ARIA authoring practices (arrow keys, Escape, Home / End).
Fix: <html lang=”en”>. Required by WCAG, breaks screen readers without it.
This list does not cover: SEO metadata (covered in a separate checklist), PWA-specific audits (most SaaS does not need these), or security headers (separate in our web-dev handbook). Scope your audit intentionally.
The reasoning behind every fix. Why this is the bar.
What we ship, with this checklist enforced from day one.
Marketing sites, SaaS builds, performance retainers.