Production Readiness: The Pre-Launch Checklist

Walk through a genuine, five-category pre-launch checklist — code quality, security, performance, accessibility, and reliability — and apply it concretely to a sample React component and app, so that shipping to production is a deliberate, verified decision rather than a hope.

'It works on my machine' is not a launch criterion. Production readiness is the discipline of verifying, category by category, that an app is actually safe to expose to real users on real networks with real data — not just that the happy path renders correctly in development. This lesson organizes that verification into a checklist covering five categories: code quality (does the code even pass its own basic checks), security (can it be exploited or does it leak something it shouldn't), performance (does it load and respond acceptably), accessibility (can everyone actually use it), and reliability (does it fail gracefully when something goes wrong, because something always eventually does). We'll walk each category against a concrete sample component, so this isn't abstract advice — it's a checklist you can literally run before every release.

Code: The Baseline That Should Never Be Manual

Before anything else, the code itself has to pass a small set of automated, non-negotiable checks — and these should run in CI on every pull request, not be something a human remembers to do before clicking deploy. tsc --noEmit (type check) catches type errors without producing build output. ESLint (lint) catches suspicious patterns — unused variables, missing dependency array entries in hooks, accessibility rule violations if you use eslint-plugin-jsx-a11y. Prettier (format) removes formatting bikeshedding from code review entirely by making formatting deterministic and automatic. And a test suite (unit tests for logic, component tests for critical UI, at least a handful of end-to-end tests for the core user flows) is the only thing that actually verifies behavior, not just syntax.

The reason this category comes first is that it's cheap, fast, and catches the largest number of trivial-but-blocking issues automatically — there is no excuse to spend expensive human review time or launch-day energy on things a script can check in seconds.

• Type check: tsc --noEmit passes with zero errors — no // @ts-ignore suppressing a real problem, no accumulated any where a real type belongs. • Lint: eslint . passes with zero errors (warnings triaged, not silently accumulating forever). • Format: prettier --check . passes, so every diff in code review is a real change, not whitespace noise. • Tests: the test suite passes in CI, covering critical business logic with unit tests and critical user flows with integration/E2E tests — not just a coverage percentage for its own sake.

Security: Assume Someone Will Try to Break It

Frontend security is easy to underrate because 'it's just the UI,' but a huge share of real-world breaches start in frontend code: a leaked API key committed to the repo, a known-vulnerable dependency, or missing HTTPS letting a request be intercepted. Before launch, scan the repository and its history for accidentally committed secrets — API keys, database URLs, private tokens — and rotate any that are found, because a git revert does not remove them from history. Run npm audit (or your package manager's equivalent) and address high/critical vulnerabilities in dependencies, especially ones that ship code to the browser.

A Content-Security-Policy (CSP) header restricts what scripts, styles, and resources a page is allowed to load, which is one of the most effective defenses against cross-site scripting (XSS) — even if an attacker manages to inject a <script> tag, a strict CSP can prevent it from ever executing or from exfiltrating data to an attacker's server. HTTPS everywhere (no mixed content, HSTS enabled) prevents network-level interception and tampering. And authentication needs a specific, deliberate review: tokens stored in httpOnly cookies rather than localStorage where possible (to reduce XSS token theft), short-lived access tokens with refresh flows, and no sensitive authorization logic that exists only in the frontend (the backend must independently enforce every permission check, because frontend checks can always be bypassed).

• No secrets in the repo — scan history, rotate anything found, keep real secrets in environment variables/secret managers, never in committed .env files. • Dependency audit — npm audit (or equivalent) run and high/critical issues resolved or explicitly, knowingly accepted. • CSP — a Content-Security-Policy header configured to restrict script/style/resource sources, mitigating XSS impact. • HTTPS — enforced everywhere, no mixed content, HSTS enabled where appropriate. • Secure auth — tokens stored to minimize XSS exposure, short-lived credentials with refresh, and every authorization decision re-verified server-side regardless of what the frontend shows or hides.

Performance: What Users Actually Feel

Performance readiness starts with a bundle analysis — run a tool like source-map-explorer or your bundler's built-in analyzer to see exactly what's in your production JavaScript bundle, because it is extremely common to accidentally ship an entire unused library, a duplicated dependency at two versions, or debug-only code into production. From there, code splitting (route-based React.lazy() + Suspense at minimum, component-level splitting for large rarely-used widgets like a rich text editor) ensures users only download the code the current page actually needs, not your whole app up front.

Image optimization (correctly sized images, modern formats like WebP/AVIF, loading="lazy" for below-the-fold images) is one of the highest-leverage performance fixes because images are usually the largest assets on a page by far. Caching (HTTP cache headers for static assets, a service worker or CDN for aggressive caching of immutable build output, sensible staleTime/cacheTime in a data-fetching library like React Query) avoids re-downloading and re-fetching things that haven't changed. And Core Web Vitals (LCP, INP, CLS) are the metrics that tie all of this together into numbers that correlate with real user-perceived speed — they should be measured against target thresholds before launch, not just checked once in a Lighthouse run and forgotten.

• Bundle analysis — know what's actually in your production JS, and remove/replace anything unexpectedly large. • Code splitting — route-level at minimum (React.lazy + Suspense), so first load only pays for the current page. • Image optimization — correct sizing, modern formats, lazy loading for offscreen images. • Caching — HTTP cache headers, CDN, and data-fetching cache configuration so repeat visits and repeat data don't re-download or re-fetch unnecessarily. • Core Web Vitals — LCP, INP, and CLS measured against target thresholds (commonly 'good' per Google's published ranges) before and after launch, not assumed.

Accessibility: Everyone Means Everyone

Accessibility is not an optional polish pass — it determines whether real users with disabilities can use the product at all, and in many jurisdictions it's a legal requirement, not just a best practice. Keyboard navigation means every interactive element — buttons, links, form fields, custom widgets like modals and dropdowns — must be reachable and operable using only Tab, Shift+Tab, Enter, Space, and arrow keys, with a visible focus indicator at every step; if a feature only works with a mouse, it doesn't work for a meaningful number of real users.

Screen reader support means the DOM structure and ARIA attributes actually convey meaning to assistive technology — test with a real screen reader (VoiceOver, NVDA, or JAWS), not just an automated linter, because automated tools catch maybe a third of real accessibility issues. Contrast means text and meaningful UI elements meet WCAG's minimum contrast ratios (4.5:1 for normal text, 3:1 for large text and UI components) — checked with a contrast checker tool against your actual design tokens, not eyeballed. Focus management means that when content changes dynamically (a modal opens, a route changes, an error appears), focus moves somewhere sensible and is never silently lost or trapped incorrectly. And semantic HTML — real <button>, <nav>, <main>, <label> elements instead of <div onClick> everywhere — gives you most of keyboard support, screen reader support, and focus behavior for free, before you write a single line of custom ARIA.

• Keyboard — every interactive element reachable and operable via keyboard alone, with a visible focus indicator. • Screen reader — verified with an actual screen reader, not just automated tooling; meaningful labels, live regions for dynamic content. • Contrast — text and interactive elements meet WCAG AA contrast ratios, checked against real design tokens. • Focus — managed deliberately on route changes, modal open/close, and dynamic content updates; never silently lost. • Semantic HTML — real interactive/structural elements used instead of generic <div>/<span> with click handlers and custom ARIA bolted on.

Reliability: Failing Gracefully, Because Failure Is Guaranteed

No production app runs error-free forever — networks drop, APIs time out, browser extensions interfere, users hit edge cases you never tested. Reliability readiness means the app is designed for that reality instead of assuming it away. Error boundaries around route-level and widget-level sections of the UI (covered in depth in the Observability module) stop one broken feature from taking down the entire page. Consistent API error handling — every data-fetching call has an explicit error path, not just a happy-path .then() — ensures a failed request produces a clear, specific message instead of a blank screen or an infinite spinner.

Retry logic (with backoff, and only for genuinely retryable failures like network errors or 5xx responses — never blindly retrying a 4xx) recovers automatically from the transient failures that are extremely common on real mobile networks. And monitoring (the error and performance monitoring built in the Observability module, actually wired up and actually being watched, with alerts configured for elevated error rates) is what turns 'we hope nothing breaks' into 'we'll know within minutes if something does.' A genuinely complete reliability story also means every async UI state is deliberately designed, not just the success case: loading, error, and empty states are each their own explicit, tested render path for any component that fetches data.

• Error boundaries — placed at route and critical-widget boundaries, tested by deliberately throwing an error in development to confirm the fallback UI actually renders. • API error handling — every fetch/mutation has an explicit error branch with a specific, actionable message, not a silent failure or a generic 'something went wrong.' • Retry — implemented with backoff for retryable failures (network errors, 5xx) and explicitly not applied to non-retryable failures (4xx). • Monitoring — error and performance monitoring wired up and alerting configured before launch, not added reactively after the first incident.

A Runnable Pre-Launch Checklist (as npm scripts)

preflight chains every code-quality and security check into one command that CI runs on every pull request — nothing in the 'Code' or 'Security' category should depend on a human remembering to run it manually. npm audit --audit-level=high fails the command (non-zero exit code) specifically on high/critical vulnerabilities, which is what you want blocking a merge — low-severity advisories shouldn't block every single PR. The bundle analyzer script is run manually before a release, not on every PR, since it's an investigative tool rather than a pass/fail gate.

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "eslint . --max-warnings=0",
    "format:check": "prettier --check .",
    "test": "vitest run",
    "audit": "npm audit --audit-level=high",
    "analyze": "source-map-explorer 'build/static/js/*.js'",
    "preflight": "npm run typecheck && npm run lint && npm run format:check && npm run test && npm run audit"
  }
}

Code Examples

This component fails the checklist on almost every axis: no TypeScript types (fails 'Code — type check'), no loading/error state so it crashes on user.avatarUrl before the fetch resolves (fails 'Reliability'), a clickable <div> instead of a real link/button with no keyboard support (fails 'Accessibility — keyboard, semantic HTML'), #999 text on a #fff background is roughly 2.8:1 contrast, well under the 4.5:1 minimum (fails 'Accessibility — contrast'), and an unvalidated raw fetch result assigned straight into state (fails 'Code — runtime validation from the previous module').

Common mistake: The single useEffect with no cleanup, no error handling, and no loading state is the most common pattern that looks fine in a demo and then fails constantly in production the moment a network request is slow or fails.

Every checklist gap in the 'before' version is fixed: TypeScript types throughout (Code), a Zod schema validating the API response (Code/Security), explicit loading and error states via React Query (Reliability), a real <a> element with keyboard support and an aria-label for screen readers (Accessibility), dark-enough text color on the stylesheet side plus alt text on the image, and loading="lazy" plus explicit width/height to avoid layout shift (Performance/CLS, Accessibility). Expected result: Passes typecheck, lint, and a manual keyboard-navigation + screen-reader pass; contrast checked against design tokens separately in CSS.

if (isPending) return <div role="status" aria-live="polite">Loading profile…</div>;

// UserProfileCard.tsx — fails several checklist items
function UserProfileCard({ userId }) {
  const [user, setUser] = useState();

  useEffect(() => {
    fetch("/api/users/" + userId)
      .then((r) => r.json())
      .then((data) => setUser(data));
  }, [userId]);

  return (
    <div onClick={() => window.location.href = "/users/" + userId}>
      <img src={user.avatarUrl} />
      <div style={{ color: "#999", background: "#fff" }}>{user.name}</div>
    </div>
  );
}
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  avatarUrl: z.string().url(),
});
type User = z.infer<typeof UserSchema>;

async function fetchUser(userId: string): Promise<User> {
  const res = await fetch(`/api/users/${userId}`);
  if (!res.ok) throw new Error(`Failed to load user: ${res.status}`);
  return UserSchema.parse(await res.json());
}

function UserProfileCard({ userId }: { userId: string }) {
  const { data: user, isPending, isError } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetchUser(userId),
  });

  if (isPending) return <div role="status" aria-live="polite">Loading profile…</div>;
  if (isError) return <div role="alert">Couldn't load this profile. Please try again.</div>;

  return (
    <a
      href={`/users/${userId}`}
      className="user-profile-card"
      aria-label={`View ${user.name}'s profile`}
    >
      <img src={user.avatarUrl} alt={`${user.name}'s avatar`} width={48} height={48} loading="lazy" />
      <span className="user-name">{user.name}</span>
    </a>
  );
}

The Five-Category Pre-Launch Gate

Each category is independently checkable — a real pre-launch review walks all five explicitly rather than assuming a pass in one implies a pass in the others.

┌─────────┐   ┌──────────┐   ┌─────────────┐   ┌───────────────┐   ┌─────────────┐
   │  CODE    │   │ SECURITY  │   │ PERFORMANCE  │   │ ACCESSIBILITY  │   │ RELIABILITY  │
   │ typecheck│   │ no secrets│   │ bundle       │   │ keyboard       │   │ error        │
   │ lint     │──▶│ dep audit │──▶│ analysis     │──▶│ screen reader  │──▶│ boundaries   │
   │ format   │   │ CSP       │   │ code split   │   │ contrast       │   │ API errors   │
   │ tests    │   │ HTTPS     │   │ images       │   │ focus          │   │ retry        │
   │          │   │ secure auth│  │ caching/CWV  │   │ semantic HTML  │   │ monitoring   │
   └─────────┘   └──────────┘   └─────────────┘   └───────────────┘   └─────────────┘
        │               │                │                  │                  │
        └───────────────┴────────────────┴──────────────────┴──────────────────┘
                                          │
                                          ▼
                              all five categories pass
                                          │
                                          ▼
                                    ready to launch

Important Rules

Run type check, lint, format check, and the test suite in CI on every pull request — never rely on a human remembering to run them before merging.

Treat 'no secrets in the repo' and 'dependency audit clean at high/critical' as hard launch blockers, not nice-to-haves — both are common real-world breach vectors.

Measure Core Web Vitals against explicit target thresholds before launch, and re-check bundle size any time a new dependency is added.

Verify accessibility with an actual keyboard-only pass and an actual screen reader, in addition to automated linting — automated tools alone catch only a fraction of real issues.

Every component that fetches data must have an explicit, tested loading state, error state, and (where relevant) empty state — 'the happy path works' is not the same as 'this is production ready.'

Wire up error and performance monitoring, with alerting, before launch — not as a reactive step after the first production incident.

Why Checklists Beat Memory

Experienced teams don't rely on individual engineers remembering all five categories under launch-day pressure — they encode the checklist as an actual CI gate (automated checks that block a merge) plus a short manual review template (keyboard pass, screen reader spot-check, staging smoke test) that a reviewer explicitly works through before approving a release. The value isn't that any single item is hard to do — it's that under deadline pressure, the items that get skipped are exactly the ones that don't show up in a quick demo: accessibility, error states, and security hygiene. A checklist that runs the same way every single time is what keeps those from silently regressing release after release.

// looks fine locally, ship it npm run preflight && manual keyboard/screen-reader pass && staging smoke test

'Works on my machine in development' verifies almost none of the five categories — it doesn't run the linter, doesn't check a production bundle's size, doesn't test with a screen reader, and doesn't exercise error/loading states, which are usually invisible in a fast local dev environment with a warm cache and no network failures.

if (loading) return null; // 'good enough' loading state if (loading) return <div role="status" aria-live="polite">Loading…</div>;

Returning null during loading is invisible to sighted users (no feedback that anything is happening, which reads as a broken/frozen page) and invisible to screen reader users too (no aria-live announcement). A real loading state is both a UX requirement and an accessibility requirement, not just a placeholder to satisfy a conditional.

Which of the following is a 'Reliability' checklist item, as distinguished from a 'Security' checklist item, in a production readiness review? A. Retry logic with backoff for transient failures like network errors and 5xx responses. B. Running a dependency audit for known vulnerabilities. C. Configuring a Content-Security-Policy header. D. Scanning the repository for accidentally committed secrets.

A. Retry logic with backoff for transient failures like network errors and 5xx responses. — Retry logic, error boundaries, explicit API error handling, and monitoring belong to the Reliability category — they're about the app failing gracefully. Dependency audits, CSP, and secret scanning belong to Security — they're about preventing exploitation and data exposure.

Why is automated accessibility linting (e.g. eslint-plugin-jsx-a11y) not sufficient on its own to verify the 'Accessibility' checklist category? A. Automated tools catch only a fraction of real accessibility issues; a real keyboard-only pass and an actual screen reader test are needed to verify usability in practice. B. Accessibility linting only works on class components, not function components. C. ESLint cannot be run in CI, so accessibility checks must always be manual. D. Automated accessibility tools always produce false positives and should be ignored entirely.

A. Automated tools catch only a fraction of real accessibility issues; a real keyboard-only pass and an actual screen reader test are needed to verify usability in practice. — Automated linting catches structural issues (like a missing alt attribute) but cannot verify whether the actual experience of navigating by keyboard or listening with a screen reader is coherent and usable — that requires an actual manual pass with those tools, in addition to automated checks.

In the 'before' sample component in this lesson, what specifically causes it to crash before the network request even resolves? A. user starts as undefined from useState() with no initial value, but the JSX immediately accesses user.avatarUrl and user.name with no loading-state check first. B. The useEffect dependency array is missing userId. C. fetch cannot be called inside useEffect. D. The component is missing a key prop.

A. user starts as undefined from useState() with no initial value, but the JSX immediately accesses user.avatarUrl and user.name with no loading-state check first. — useState() with no argument initializes user as undefined. Because there's no if (!user) return ... guard before the JSX, the very first render — before the fetch has had any chance to resolve — tries to read user.avatarUrl, which throws immediately. This is exactly why an explicit loading state is a Reliability checklist requirement, not an optional nicety.

Take the 'before' UserProfileCard example from this lesson and, without looking at the 'after' version, list which specific checklist item from each of the five categories (Code, Security, Performance, Accessibility, Reliability) it violates, and name the concrete fix for each.

Code: no TypeScript prop types → add { userId }: { userId: string } and a Zod-validated fetch. Security: raw unvalidated data trusted directly → validate with a schema. Performance: image has no loading="lazy" or explicit dimensions → add both to avoid layout shift and unnecessary eager loads. Accessibility: clickable <div> with no keyboard support and no alt text, plus insufficient text contrast → use a real <a>/<button>, add alt, fix the color pair to meet 4.5:1. Reliability: no loading or error state, so it crashes before the fetch resolves and again on any fetch failure → add explicit isPending/isError branches.

Walk the component prop-by-prop and line-by-line against the bullet lists in each of the five sections above — for example, check the image tag against both the Performance and Accessibility bullets separately, since it fails items in both categories for different reasons.