engineering-frontend-developer
Frontend Development Guide
Overview
This guide covers modern frontend development with React, Vue, Angular, and Svelte, including component architecture, performance optimization, accessibility, and testing. Use it when building web applications, component libraries, or optimizing frontend performance.
Framework and Layout Decision Rules
- When choosing a framework, match it to team expertise and project constraints; default to React for broad ecosystem needs, Vue for progressive enhancement into existing pages, Svelte for bundle-size-critical apps, and Angular when the project requires an opinionated full-framework with built-in DI and routing.
- When implementing a design, use CSS Grid for two-dimensional page layouts and Flexbox for one-dimensional component alignment; avoid absolute positioning for layout purposes because it breaks responsive reflow.
- When building a component library, expose each component as a named export with TypeScript props interface, a Storybook story, and a unit test -- components without all three are not merged.
- When integrating with backend APIs, centralize fetch logic in a typed API client layer (e.g., a single
api.tsmodule usingfetchoraxioswith interceptors) so auth headers, error transforms, and retries are handled in one place.
Performance Decision Rules
- When a page's Largest Contentful Paint exceeds 2.5 seconds in Lighthouse CI, treat it as a blocking bug -- profile with Chrome DevTools Performance tab and fix the largest bottleneck before merging.
- When adding animations, use CSS
transformandopacityproperties (compositor-only) rather thanwidth,height, ortop/leftto avoid triggering layout recalculations that cause jank. - When the app needs offline support, register a service worker with a cache-first strategy for static assets and a network-first strategy for API requests, falling back to cached responses when offline.
- When initial JS bundle exceeds the budget (e.g., 200 KB gzipped), add route-based code splitting with
React.lazy()or dynamicimport()and defer non-critical scripts below the fold. - When supporting older browsers, define a browserslist config and let the build tool (Vite, Webpack) auto-polyfill; do not manually add polyfills or feature checks unless browserslist coverage is insufficient.
- When adding images, use
<picture>with WebP/AVIF sources and explicitwidth/heightattributes to prevent layout shift; for images below the fold, addloading="lazy". - When a route is not needed on initial page load, wrap it in
React.lazy()(or framework equivalent) with a<Suspense>fallback so the main bundle excludes that route's code. - When serving static assets, configure the CDN or server to set
Cache-Control: public, max-age=31536000, immutableon content-hashed filenames andno-cacheonindex.html.
Accessibility Decision Rules
- When building any interactive component, add ARIA attributes, keyboard handlers (
Enter,Space,Escapeas appropriate), and test with axe-core before marking the task complete. - When building forms, associate every
<input>with a<label>viahtmlFor/id, provide visible error messages linked witharia-describedby, and ensure the form is fully operable with keyboard-only navigation. - When using color to convey meaning (e.g., error states, status badges), always include a secondary indicator (icon, text, pattern) so color-blind users can distinguish states.
- When adding a modal or dropdown, trap focus inside the element while it is open and return focus to the trigger element on close; test by tabbing through the entire flow without a mouse.
- When a pull request adds a new interactive component, the PR must include an axe-core integration test that asserts zero WCAG 2.1 AA violations before it can be merged.
CSS Debugging Decision Rules
When a layout breaks, diagnose by symptom:
- Flex child overflows its container: Add
min-width: 0(row) ormin-height: 0(column) to the flex child. Flex items default tomin-width: auto, which prevents shrinking below content size. - Grid items ignore column width: Use
minmax(0, 1fr)instead of1fr. Plain1frmeansminmax(auto, 1fr), which lets content push the column wider than intended. position: stickydoes not stick: Check every ancestor foroverflow: hidden,overflow: auto, oroverflow: scroll. Any of these creates a new scrolling context that contains the sticky element. Also verify the element has atop/bottomvalue set.- Element centered with
margin: autodoes not center: Verify the element has an explicitwidth(block) or the parent hasdisplay: flex(flex child).margin: autodoes nothing on full-width block elements. z-indexdoes not work: The element needsposition: relative/absolute/fixed/stickyto create a stacking context. Also check if an ancestor creates a stacking context (viatransform,opacity < 1,filter, orwill-change) that limits the z-index scope.- Gap between inline-block/inline elements: Whitespace in HTML creates gaps. Fix: use
display: flexon the parent (preferred), or setfont-size: 0on parent and reset on children. - 1px gap between adjacent elements: Sub-pixel rendering. Fix: use
outlineinstead ofborderfor debugging, or setbackgroundon the parent to mask the gap. - Text truncation with ellipsis not working: Requires all three:
overflow: hidden,white-space: nowrap,text-overflow: ellipsis. For multi-line truncation, use-webkit-line-clampwithdisplay: -webkit-boxand-webkit-box-orient: vertical. - Container queries not applying: Verify the ancestor has
container-type: inline-size(orsize). The element itself cannot be its own container — the query must reference a parent.
Build Tooling Decision Rules
- Vite (default): Use for all new projects. Instant dev server via native ESM, fast HMR, Rollup-based production builds. If the project uses CommonJS-only dependencies that fail with Vite, add
optimizeDeps.includefor those packages — do not switch to Webpack. - Webpack: Use only for existing projects that already use it, or when a specific Webpack-only plugin has no Vite equivalent (rare). Migrate to Vite when the opportunity arises (see Migration Decision Rules).
- Turbopack: Use only inside Next.js 14+ (
next dev --turbo). Not yet usable standalone. Provides faster dev builds than Webpack for Next.js projects. If builds are already fast (<5s), do not bother switching. - esbuild: Use for library bundling, scripts, or CLI tools — not for full applications. No HTML entry point handling, no code splitting by route. Use
tsup(esbuild wrapper) for publishing npm packages. - SWC vs Babel: Use SWC (via Vite plugin or Next.js default). SWC is 20-70x faster for transforms. Use Babel only if a project depends on a Babel-only plugin with no SWC equivalent (increasingly rare).
- Monorepo tooling: <5 packages = npm/pnpm workspaces, no extra tool. 5-20 packages = Turborepo for task caching. >20 packages or polyglot = Nx. Never add monorepo tooling to a single-package project.
SSR and Server Components Decision Rules
When to Use SSR vs CSR vs SSG
- SSG (Static Site Generation): Use for content that changes less than once per hour (marketing pages, docs, blog posts). Build at deploy time. Fastest possible TTFB.
- SSR (Server-Side Rendering): Use for personalized content (dashboards, user profiles), SEO-critical pages with dynamic data, or pages where stale data is unacceptable. Adds server latency to every request — cache aggressively with
Cache-Controlor CDN. - CSR (Client-Side Rendering): Use for authenticated-only pages (admin panels, internal tools) where SEO does not matter and interactivity is the priority. Simplest to build and deploy (static hosting).
- ISR (Incremental Static Regeneration): Use for pages that are mostly static but need periodic updates (product listings, pricing pages). Set
revalidateinterval based on how stale the data can be: 60s for prices, 3600s for blog posts.
React Server Components (RSC) Decision Rules
- Default to Server Components: Every component is a Server Component unless it needs interactivity. If a component has
onClick,useState,useEffect, or browser APIs — it must be a Client Component ('use client'). - Data fetching: Fetch data in Server Components with
async/awaitdirectly — nouseEffect, no TanStack Query, no loading states needed. The data is resolved before the HTML is sent. - Auth in Server Components: Read the session/cookie in a Server Component and pass the user object as a prop to Client Components. Never read cookies or sessions in Client Components — they execute on the client.
- When to use Server Actions vs API routes: Use Server Actions (
'use server') for form submissions and mutations that are called from the UI. Use API routes (/api/*) for webhooks, third-party integrations, or endpoints called by external services. - Sharing state between Server and Client Components: Pass serializable data as props from Server → Client. If you need client-side state derived from server data, initialize
useStatewith the server prop and manage updates on the client. - Large data sets: Stream with
<Suspense>boundaries. Wrap the slow Server Component in<Suspense fallback={<Skeleton />}>. The shell renders immediately; the slow component streams in when ready. Place Suspense boundaries at meaningful UI sections (sidebar, main content, comments) — not around every component.
Hydration Error Decision Tree
- Error: "Text content does not match": A value differs between server and client render. Common causes:
Date.now(),Math.random(), ornew Date().toLocaleString()in render → move touseEffect+useState.typeof window !== 'undefined'conditional rendering → useuseEffectto set ahasMountedflag.- Browser extensions injecting DOM nodes → not your bug, but test in incognito to confirm.
- Error: "Hydration failed because the server rendered HTML didn't match": A structural difference (different elements). Common causes:
<p>nested inside<p>, or<div>inside<p>→ fix the nesting, HTML spec does not allow it.- A third-party component renders differently on server vs client → wrap in
dynamic(() => import(...), { ssr: false }). - Missing closing tags or self-closing tags where HTML requires explicit close.
Performance Debugging Workflow
When a page is slow, follow this sequence — do not skip steps:
Step 1: Identify the bottleneck type
- Run Lighthouse. Check which metric is failing:
- LCP >2.5s: The largest visible element loads too slowly. Go to Step 2.
- FID/INP >200ms: User interaction is blocked by JavaScript. Go to Step 3.
- CLS >0.1: Layout shifts after initial paint. Go to Step 4.
Step 2: Fix LCP
- Check what the LCP element is (Lighthouse shows it). Usually: hero image, heading text, or video.
- If image: Add
priority(Next.js) orfetchpriority="high". Add explicitwidth/height. Serve in WebP/AVIF. If served from a CDN, verify cache headers. - If text: Check for render-blocking fonts. Use
font-display: swaporfont-display: optional. Preload the critical font file with<link rel="preload" as="font" crossorigin>. - If blocked by JS: The bundle is too large. Check Step 3. The LCP element cannot render until the JS that creates it is loaded and executed.
- Check the server response time (TTFB). If TTFB >600ms, the bottleneck is backend or CDN — not frontend.
Step 3: Fix INP (Interaction to Next Paint)
- Open Chrome DevTools → Performance → record a click/type interaction.
- Find the Long Task (>50ms yellow bar). Click it to see the call stack.
- If the long task is your code: Break the work into smaller chunks with
requestIdleCallback,scheduler.yield()(Chrome 115+), orsetTimeout(fn, 0)to yield to the browser between frames. - If the long task is React re-rendering: Open React DevTools Profiler. Find the component that re-renders. Common fixes:
React.memo(expensive children),useMemo(derived values), or move state closer to where it is used. If >500 components re-render on a single state change, the state is too high in the tree. - If the long task is third-party script (analytics, ads): Defer with
asyncordeferattribute, or load afterrequestIdleCallback.
Step 4: Fix CLS
- Check which element shifts (Lighthouse shows it, or use DevTools → "Layout Shift Regions").
- Image/video without dimensions: Add
widthandheightattributes (oraspect-ratioCSS). The browser reserves space before the asset loads. - Font swap: The fallback font has different metrics than the web font. Use
size-adjustin@font-faceto match metrics, or usefont-display: optionalto eliminate the swap entirely. - Dynamic content injected above the fold: Ads, banners, cookie notices. Reserve space with
min-heighton the container. If the content height varies, usecontain: layoutto prevent shifts from propagating. - Late-loading CSS: If a stylesheet loads after first paint and changes visible layout, inline the critical CSS or preload the stylesheet.
Code Quality Decision Rules
- When writing tests, require every component to have at least one unit test covering its primary render path and one interaction test (click, keyboard) -- enforce via a CI coverage gate of 80% line coverage minimum.
- When starting a project, enable
strict: trueintsconfig.jsonon day one; retrofitting strict mode later is exponentially harder as the codebase grows. - When an API call or async operation fails, display a user-facing error message with a retry action -- never swallow errors silently or show raw exception text.
- When a component file exceeds 300 lines, split it into smaller sub-components with a shared barrel export; large files signal mixed responsibilities.
- When setting up CI, include lint (ESLint), type-check (
tsc --noEmit), test (Vitest/Jest), and bundle-size check as required gates -- all four must pass before merge.
Workflow
Step 1: Project Setup and Architecture
- Set up modern development environment with proper tooling.
- Configure build optimization and performance monitoring.
- Establish testing framework and CI/CD integration.
- Create component architecture and design system foundation.
Step 2: Component Development
- Create reusable component library with proper TypeScript types.
- Implement responsive design with mobile-first approach.
- Build accessibility into components from the start.
- Create comprehensive unit tests for all components.
Step 3: Performance Optimization
- Implement code splitting and lazy loading strategies.
- Optimize images and assets for web delivery.
- Monitor Core Web Vitals and optimize accordingly.
- Set up performance budgets and monitoring.
Step 4: Testing and Quality Assurance
- Write comprehensive unit and integration tests.
- Perform accessibility testing with real assistive technologies.
- Test cross-browser compatibility and responsive behavior.
- Implement end-to-end testing for critical user flows.
Reference
Lighthouse CI Targets
- Performance score: 90+
- All interactive elements keyboard-navigable with visible focus indicators
- axe-core: zero violations at WCAG 2.1 AA level
- Bundle size: < 200 KB gzipped for initial JS (enforce with
bundlesizeor equivalent) - Zero TypeScript
anycasts in production code;strict: trueenabled in tsconfig
Migration Decision Rules
- CRA to Vite: Migrate when CRA's build time exceeds 30s or eject pressure mounts. Steps: replace
react-scriptswithvite+@vitejs/plugin-react, moveindex.htmlto root, updateprocess.env→import.meta.env, fix any CommonJS imports. Budget: 2-4 hours for small apps, 1-2 days for large apps with custom Webpack config. - Pages Router to App Router (Next.js): Migrate incrementally. Move one route at a time to
app/directory. Start with static/simple pages, leave complex pages (with getServerSideProps + complex state) for last. Do not migrate the entire app at once. Budget: 1 route per day for complex apps. - JavaScript to TypeScript: Enable
allowJs: truein tsconfig and rename files one at a time from.js→.tsx. Start with leaf components (no dependencies), work inward. Enforcestrict: trueon new files from day one; create atsconfig.strict.jsonthat extends base if needed. Budget: ~10 files per day. - CSS Modules to Tailwind: Do not rewrite all styles at once. Install Tailwind alongside CSS Modules. New components use Tailwind; migrate old components only when you touch them for other reasons. Budget: per-component during normal development.
- Class components to hooks: Migrate only when you need to modify the component for a feature change. Do not migrate stable, untouched class components — they work fine. When migrating:
componentDidMount→useEffect(..., []),componentDidUpdate→useEffectwith deps,this.state→useState.
State Management Decision Matrix
- <5 components sharing state: Lift state up with props. No library needed. If prop drilling goes >3 levels, add React Context for that specific slice.
- 5-15 components with shared state: Use React Context +
useReducerfor structured state, or Zustand for simpler API. If the state is server data, use TanStack Query instead — not Context. - >15 components or complex derived state: Zustand (simple, small bundle) or Jotai (atomic, bottom-up). Use Redux Toolkit only if the team already knows Redux. Never introduce Redux to a new project.
- Server state (API data, caching, sync): TanStack Query (React), SWR (simpler needs), or Apollo Client (GraphQL). Never store server data in a client state manager (Zustand, Redux) — it causes stale data and duplicate cache management.
- Form state: react-hook-form for complex forms (>5 fields, validation, dynamic fields). Native
useStatefor simple forms (<5 fields, no complex validation). Never use a global state manager for form state. - URL state (filters, pagination, search): Use URL search params (
useSearchParams) as the source of truth. Sync to component state only for derived values. This makes the state shareable, bookmarkable, and back-button friendly.
Self-Verification Protocol
After completing a frontend implementation, verify:
- Run Lighthouse CI and confirm: Performance >=90, Accessibility >=90, Best Practices >=90. If any score drops below 90, fix before merge.
- Run
npx axe-coreoraxe-playwrighton every new page/component. Zero WCAG 2.1 AA violations. - Test keyboard navigation: Tab through the entire page. Every interactive element must be reachable and operable. Focus indicators must be visible.
- Open the browser DevTools Performance tab, record a user interaction, and check for: layout thrashing (forced reflows), long tasks >50ms, excessive re-renders.
- Check bundle size: run
npx bundlesizeor check the build output. If initial JS exceeds 200KB gzipped, investigate what is in the bundle (usenpx source-map-explorer). - Test responsive layout at 320px, 768px, and 1280px. No horizontal scroll, no overlapping elements, no unreadable text.
- Test with browser devtools network throttling set to "Slow 3G." If the page is unusable, add loading states and optimize critical rendering path.
Failure Recovery
- Component re-renders excessively: Use React DevTools Profiler to identify the cause. Common fixes: memoize with
React.memo(for expensive children),useMemo/useCallback(for derived values/callbacks passed as props), or move state closer to where it is used. - Bundle size suddenly increased: Run
source-map-explorerto find the culprit. Common causes: importing an entire library (import _ from 'lodash'→import groupBy from 'lodash/groupBy'), accidentally bundling a dev dependency, or a heavy polyfill. - Hydration mismatch (SSR/SSG): The server-rendered HTML differs from client render. Common causes: using
Date.now()orMath.random()in render, accessingwindow/documentduring SSR, or conditional rendering based on client-only state. Fix: useuseEffectfor client-only values, orsuppressHydrationWarningas last resort. - CSS layout breaks at specific viewport: Check for: hardcoded pixel widths, missing
min-width: 0on flex children,overflow: hiddenclipping content, orposition: absoluteelements escaping their container. Use CSS Grid withminmax()instead of fixed widths. - API calls fire too many times: Check for missing or incorrect
useEffectdependency arrays. If using TanStack Query, setstaleTimeto avoid refetching on every mount. If usinguseEffectfor data fetching, consider switching to a data fetching library entirely.
Existing Codebase Orientation
When joining an existing frontend project:
- Run
npm start/npm run dev(5 min) — If it fails, fix the dev environment first. - Check the tech stack (5 min) — Framework (React/Vue/Angular/Svelte), bundler (Vite/Webpack/Turbopack), CSS approach (Tailwind/CSS Modules/styled-components), state management, testing library.
- Run the test suite (5 min) —
npm test. Note coverage, test types, and which flows are untested. - Open the browser DevTools (10 min) — Check bundle size (Network tab), console errors, Lighthouse score. These are your baseline numbers.
- Read the component tree (10 min) — Start from the app root. Map the top-level routes and their key components. Identify shared components (design system, layout).
- Check for tech debt signals (5 min) — Any
anycasts?eslint-disablecomments? Files >500 lines? Inline styles? These indicate areas of fragility. - Identify the data flow (10 min) — How does data get from the API to the UI? Is there a centralized API client? Are there loading/error states? Is server state managed with a library or manually?
Scripts
scripts/check_bundle.sh-- Analyze a build output directory for JS and CSS bundle sizes, with gzip estimates and configurable warning/error thresholds. Run with--helpfor options.
See DataTable Component for a full virtualized DataTable implementation in React + TypeScript. See React Component Patterns for compound components, custom hooks, error boundaries, optimistic UI, and form validation patterns. See TypeScript Patterns for discriminated unions, polymorphic components, branded types, and type-safe API clients. See CSS Patterns for modern CSS Grid layouts, container queries, theming, view transitions, and :has() selectors.