- Network & Delivery
- JavaScript Performance
- React-Specific Optimizations
- CSS & Rendering
- Images & Media
- Browser Internals
- Core Web Vitals
- Caching & Offline
- Performance Measurement
- XSS (Cross-Site Scripting)
- CSRF (Cross-Site Request Forgery)
- CORS (Cross-Origin Resource Sharing)
- Authentication & Authorization
- JWT Security
- Clickjacking
- Content Security Policy
- HTTPS & MITM
- Secure Cookies
- Frontend Security Best Practices
- WCAG Principles (POUR)
- Semantic HTML vs ARIA
- Keyboard Accessibility
- Color & Contrast
- Accessible React Patterns
- Forms Accessibility
- Testing Tools
Web performance directly impacts user experience, conversion rates, and SEO rankings. Studies show:
- 53% of mobile users abandon sites that take longer than 3 seconds to load
- 1 second delay can reduce conversions by 7%
- Google uses performance as a ranking factor
The key is understanding the critical rendering path and optimizing each stage.
The network is often the biggest bottleneck. Every byte transmitted costs time, especially on slow connections. HTTP requests have overhead (DNS lookup, TCP handshake, TLS negotiation), so reducing requests and payload size yields massive gains.
Goal: Send less, send faster.
Minify + Compress
- Minification: Removes whitespace, comments, shortens variable names
- Gzip: ~70% compression, universally supported
- Brotli: ~80% compression, better for static assets, requires HTTPS
Content-Encoding: brRemove Unused JS/CSS (Tree-shaking)
- Dead code elimination at build time
- Works with ES6 modules
- Webpack, Rollup, Vite do this automatically
Avoid Huge JSON Responses
- Paginate API responses
- Use GraphQL to request only needed fields
- Consider compression for large payloads
HTTP/2 / HTTP/3
- Multiplexing: Multiple requests over single connection
- Header compression: Reduces overhead
- Server push: Proactively send resources
- HTTP/3: Built on QUIC, faster connection establishment
CDN for Static Assets
- Geographically distributed servers
- Lower latency (closer to users)
- Edge caching
- Offloads origin server
Cache Aggressively
Cache-Control: public, max-age=31536000, immutablepublic: Can be cached by CDNsmax-age: Time in secondsimmutable: Never revalidate (for versioned assets)
<!-- Preload critical resources -->
<link rel="preload" as="script" href="/app.js">
<link rel="preload" as="style" href="/critical.css">
<link rel="preload" as="font" href="/font.woff2" crossorigin>
<!-- Establish early connections -->
<link rel="preconnect" href="https://cdn.com">
<link rel="dns-prefetch" href="https://fonts.googleapis.com">- preload: High-priority fetch for current page
- preconnect: Warm up connection (DNS + TCP + TLS)
- dns-prefetch: DNS resolution only (fallback for older browsers)
JavaScript is the most expensive resource on modern web. It must be downloaded, parsed, compiled, and executed - all on the main thread. Large JS bundles block rendering and delay interactivity.
Goal: Parse & execute less JS.
Break large bundles into smaller chunks loaded on-demand.
Route-based Splitting
const Dashboard = React.lazy(() => import("./Dashboard"));
const Profile = React.lazy(() => import("./Profile"));
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>Component-based Splitting
const HeavyChart = React.lazy(() => import("./HeavyChart"));<!-- Deferred: downloads in parallel, executes after HTML parsing -->
<script src="analytics.js" defer></script>
<!-- Async: downloads in parallel, executes immediately -->
<script src="ads.js" async></script>When to use:
defer: Scripts that need DOM (most scripts)async: Independent scripts (analytics, ads)
Long Tasks (>50ms) freeze the UI.
Break Long Tasks
// Bad: blocks for 500ms
for (let i = 0; i < 100000; i++) {
processItem(i);
}
// Good: yields to browser
async function processInChunks() {
for (let i = 0; i < 100000; i += 100) {
for (let j = 0; j < 100; j++) {
processItem(i + j);
}
await new Promise(resolve => setTimeout(resolve, 0));
}
}requestIdleCallback
requestIdleCallback(() => {
// Non-urgent work
prefetchNextPage();
});Web Workers
// main.js
const worker = new Worker('heavy.js');
worker.postMessage({ data });
worker.onmessage = (e) => updateUI(e.data);
// heavy.js
self.onmessage = (e) => {
const result = expensiveComputation(e.data);
self.postMessage(result);
};React's virtual DOM is efficient, but unnecessary re-renders still waste CPU. Each render involves executing component functions, diffing virtual DOM, and potentially updating real DOM.
Goal: Fewer renders, cheaper renders.
React.memo - Memoizes component
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{expensiveComputation(data)}</div>;
});useCallback - Memoizes functions
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);useMemo - Memoizes values
const sortedData = useMemo(() => {
return data.sort((a, b) => a.value - b.value);
}, [data]);startTransition - Marks updates as non-urgent
import { startTransition } from 'react';
function handleSearch(query) {
setInputValue(query); // Urgent
startTransition(() => {
setSearchResults(search(query)); // Non-urgent
});
}useDeferredValue - Shows stale value while computing new one
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = search(deferredQuery);
return (
<div style={{ opacity: query !== deferredQuery ? 0.5 : 1 }}>
{results.map(r => <Item key={r.id} {...r} />)}
</div>
);
}Only render visible items.
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={10000}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>Item {index}</div>
)}
</FixedSizeList>Libraries:
react-window(lightweight)react-virtualized(feature-rich)
The browser rendering pipeline: Layout → Paint → Composite. Some CSS properties trigger expensive recalculations of the entire layout. Understanding which operations are cheap vs expensive is critical.
Goal: Avoid layout thrashing & minimize repaint cost.
Inline Above-the-Fold CSS
- Prevents render-blocking
- First paint happens faster
<head>
<style>
/* Critical CSS inlined */
.hero { background: blue; }
</style>
<link rel="preload" href="/full.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>Expensive (triggers layout):
// ❌ Forces reflow
el.style.top = "10px";
el.style.width = "100px";Cheap (compositor only):
// ✅ Only compositing
el.style.transform = "translateY(10px)";
el.style.opacity = 0.5;Property Costs:
- Layout:
width,height,margin,padding,top,left,font-size - Paint:
color,background,box-shadow - Composite only:
transform,opacity
.card {
will-change: transform;
}Purpose: Creates a new composite layer before animation starts.
Warning: Overuse wastes memory. Remove after animation:
el.style.willChange = 'transform';
el.addEventListener('animationend', () => {
el.style.willChange = 'auto';
});Images typically account for 50%+ of page weight. Modern formats and lazy loading can cut this dramatically with minimal effort.
Goal: Smaller images, loaded later.
- WebP: ~30% smaller than JPEG, wide support
- AVIF: ~50% smaller, growing support
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture><img
src="small.jpg"
srcset="small.jpg 480w, medium.jpg 768w, large.jpg 1024w"
sizes="(max-width: 600px) 480px, (max-width: 900px) 768px, 1024px"
alt="Responsive image"
/>- srcset: Available image sizes
- sizes: Expected display size
- Browser picks best option
<img src="image.jpg" loading="lazy" alt="Lazy loaded">Native lazy loading delays download until near viewport.
Eager vs Lazy:
- Above-the-fold:
loading="eager"(default) - Below-the-fold:
loading="lazy"
Understanding how browsers render pages helps you optimize effectively.
The Pipeline:
-
HTML → DOM
- Browser parses HTML into DOM tree
-
CSS → CSSOM
- Browser parses CSS into CSSOM tree
-
DOM + CSSOM → Render Tree
- Combines trees, excludes hidden elements
-
Layout
- Calculates exact position and size
-
Paint
- Fills in pixels (text, colors, images, shadows)
-
Composite
- Combines layers in correct order
🔴 Render-blocking:
- CSS blocks rendering (needs CSSOM before render tree)
- Synchronous JS blocks parsing
🟢 Non-blocking:
async/deferscripts- Images, fonts (don't block render)
<!-- Critical CSS inline -->
<style>/* Critical styles */</style>
<!-- Async load full CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
<!-- Defer non-critical JS -->
<script src="app.js" defer></script>Google's Core Web Vitals measure real user experience. They directly impact SEO and conversion. Understanding these metrics and how to improve them is essential for modern web development.
| Metric | Meaning | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | ≤ 2.5s | 2.5s - 4s | > 4s |
| INP | Interaction to Next Paint | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS | Cumulative Layout Shift | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
What: Time until the largest visible element loads
Measures: Loading performance
Common Causes:
- Slow server response (TTFB)
- Large, unoptimized images
- Render-blocking JavaScript/CSS
- Client-side rendering delays
How to Fix:
<!-- Optimize images -->
<img src="hero.webp" fetchpriority="high" alt="Hero">
<!-- Preload critical resources -->
<link rel="preload" as="image" href="hero.webp">
<!-- Use CDN -->
<img src="https://cdn.example.com/hero.webp">Server-side:
- Reduce TTFB with caching, CDN
- Use SSR or SSG for critical content
- Optimize database queries
What: Delay between user interaction and UI update (replaced FID in 2024)
Measures: Responsiveness
Common Causes:
- Long JavaScript tasks (>50ms)
- Heavy event handlers
- Expensive re-renders
- Main thread blocking
How to Fix:
// Break long tasks
async function processLargeList(items) {
for (let i = 0; i < items.length; i += 50) {
processBatch(items.slice(i, i + 50));
await scheduler.yield(); // Let browser breathe
}
}
// Use React 18 transitions
import { startTransition } from 'react';
function handleFilter(value) {
setInputValue(value); // Immediate
startTransition(() => {
setFilteredResults(filter(value)); // Deferred
});
}
// Debounce expensive operations
const debouncedSearch = useDeferredValue(searchQuery);Code splitting:
const HeavyComponent = lazy(() => import('./Heavy'));What: Unexpected layout movement while page loads
Measures: Visual stability
Common Causes:
- Images without dimensions
- Ads, embeds, iframes injected dynamically
- Web fonts causing FOUT/FOIT
- Animations that trigger layout
How to Fix:
<!-- Always set dimensions -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- Or use aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 16/9; width: 100%;">
<!-- Reserve space for ads -->
<div class="ad-slot" style="min-height: 250px;">
<!-- Ad loads here -->
</div>
<!-- Prevent font flash -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<style>
@font-face {
font-family: 'MyFont';
font-display: swap; /* or optional */
src: url('font.woff2');
}
</style>CSS transforms (don't trigger layout):
/* ❌ Causes layout shift */
.element {
top: 0;
transition: top 0.3s;
}
.element:hover {
top: -10px;
}
/* ✅ No layout shift */
.element {
transform: translateY(0);
transition: transform 0.3s;
}
.element:hover {
transform: translateY(-10px);
}TTFB – Time to First Byte
- Server response time
- Target: < 800ms
- Improve with: caching, CDN, database optimization
FCP – First Contentful Paint
- First pixel rendered
- Target: < 1.8s
- Improve with: inline critical CSS, reduce blocking resources
TBT – Total Blocking Time
- Lab metric (Lighthouse)
- Sum of blocking time from long tasks
- Correlates with INP
Field Data (Real Users):
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
// Send to analytics
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
navigator.sendBeacon('/analytics', body);
}
onLCP(sendToAnalytics);Lab Data (Testing):
- Lighthouse in Chrome DevTools
- Chrome UX Report (CrUX) - real user data
- PageSpeed Insights - combines lab + field data
- WebPageTest - detailed waterfall analysis
Tools:
- Chrome DevTools → Performance tab
- Google Search Console → Core Web Vitals report
- web.dev/measure
Caching eliminates network requests entirely - the fastest request is no request. Service Workers enable sophisticated caching strategies and offline experiences.
// sw.js - Cache-first strategy
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/styles.css',
'/app.js',
'/logo.png'
]);
})
);
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(response => {
return response || fetch(e.request);
})
);
});1. Cache First
// Try cache, fallback to network
caches.match(request) || fetch(request)Use for: Static assets, images
2. Network First
// Try network, fallback to cache
fetch(request).catch(() => caches.match(request))Use for: API calls, dynamic content
3. Stale-While-Revalidate
const cached = await caches.match(request);
const fetching = fetch(request).then(res => {
cache.put(request, res.clone());
return res;
});
return cached || fetching;Use for: Balance freshness & speed
const db = await openDB('my-db', 1, {
upgrade(db) {
db.createObjectStore('posts');
}
});
await db.put('posts', data, 'post-123');
const post = await db.get('posts', 'post-123');"You can't improve what you don't measure." Performance monitoring should be continuous, not one-time.
Chrome DevTools → Performance Tab
- Record real interactions
- See flame charts, long tasks
- Identify bottlenecks
Lighthouse
- Automated audits
- Performance score + suggestions
- Built into Chrome DevTools
WebPageTest
- Test from different locations
- Connection throttling
- Filmstrip view, waterfall
React Profiler
import { Profiler } from 'react';
<Profiler id="App" onRender={callback}>
<App />
</Profiler>
function callback(id, phase, actualDuration) {
console.log(`${id} took ${actualDuration}ms`);
}Long Tasks (>50ms)
- Block user interactions
- Split into smaller chunks
JS Parse/Execute Time
- Large bundles slow startup
- Code split aggressively
Layout Shifts
- Measure CLS
- Set image dimensions
Network Waterfalls
- Identify blocking resources
- Check compression, caching
- Look for request chains
{
"budgets": [{
"resourceType": "script",
"budget": 300
}, {
"resourceType": "total",
"budget": 500
}]
}Set budgets, fail builds if exceeded.
Security is layered. No single measure is perfect, but combining multiple strategies creates robust protection. Always assume user input is malicious and verify everything server-side.
XSS allows attackers to inject malicious JavaScript into your site, running in the context of your domain with full access to cookies, localStorage, and the DOM.
Attack Vector: Attacker injects malicious JS into your site
1. Stored XSS
- Saved in database
- Affects all users who view it
- Example: Comment systems, user profiles
2. Reflected XSS
- Via URL/query parameters
- Affects single user (via phishing link)
- Example: Search results, error messages
3. DOM XSS
- Unsafe client-side JS manipulation
- Never reaches server
- Example:
innerHTML,eval()
// ❌ DANGEROUS
const userInput = "<img src=x onerror='alert(1)'>";
div.innerHTML = userInput; // XSS!
// ❌ DANGEROUS
const search = new URLSearchParams(location.search).get('q');
document.write(search); // XSS!1. Escape Output (React does this automatically)
// ✅ Safe - React escapes by default
<div>{userInput}</div>
// ❌ Dangerous - bypasses escaping
<div dangerouslySetInnerHTML={{__html: userInput}} />2. Sanitize Input
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
div.innerHTML = clean; // ✅ Safe3. Content Security Policy
Content-Security-Policy: default-src 'self'; script-src 'self'4. Use textContent over innerHTML
// ✅ Safe
element.textContent = userInput;
// ❌ Dangerous
element.innerHTML = userInput;CSRF exploits the fact that browsers automatically send cookies with every request. An attacker tricks your browser into making authenticated requests to another site where you're logged in.
Attack Flow:
- User logs into
bank.com - User visits
evil.com evil.comsubmits form tobank.com/transfer- Browser sends cookies → request succeeds!
<!-- On evil.com -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>1. CSRF Tokens
// Server generates token
const token = generateCSRFToken();
res.cookie('csrf', token);
// Client includes in requests
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrf')
}
});
// Server validates
if (req.headers['x-csrf-token'] !== req.cookies.csrf) {
throw new Error('CSRF validation failed');
}2. SameSite Cookies
Set-Cookie: sessionId=abc; SameSite=Strict; Secure- Strict: Never sent on cross-site requests
- Lax: Sent on top-level GET navigation
- None: Always sent (requires Secure)
3. Custom Headers (JWT approach)
// Browser won't send custom headers cross-origin
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});CORS is a browser security mechanism, not server protection. It prevents malicious sites from reading responses from your API via JavaScript.
Key Point: CORS protects users, not your API.
Browser → OPTIONS preflight → Server
Server → Access-Control-Allow-Origin → Browser
Browser → Actual request → Server (only if allowed)
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400# ❌ INVALID - can't use * with credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# ✅ VALID
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true- Same-origin requests
<img>,<script>,<link>tags- Server-to-server requests
| Concept | Meaning | Example |
|---------|---------|---------|only:** transform, opacity
.card {
will-change: transform;
}Purpose: Creates a new composite layer before animation starts.
Warning: Overuse wastes memory. Remove after animation:
el.style.willChange = 'transform';
el.addEventListener('animationend', () => {
el.style.willChange = 'auto';
});Images typically account for 50%+ of page weight. Modern formats and lazy loading can cut this dramatically with minimal effort.
Goal: Smaller images, loaded later.
- WebP: ~30% smaller than JPEG, wide support
- AVIF: ~50% smaller, growing support
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture><img
src="small.jpg"
srcset="small.jpg 480w, medium.jpg 768w, large.jpg 1024w"
sizes="(max-width: 600px) 480px, (max-width: 900px) 768px, 1024px"
alt="Responsive image"
/>- srcset: Available image sizes
- sizes: Expected display size
- Browser picks best option
<img src="image.jpg" loading="lazy" alt="Lazy loaded">Native lazy loading delays download until near viewport.
Eager vs Lazy:
- Above-the-fold:
loading="eager"(default) - Below-the-fold:
loading="lazy"
Understanding how browsers render pages helps you optimize effectively.
The Pipeline:
-
HTML → DOM
- Browser parses HTML into DOM tree
-
CSS → CSSOM
- Browser parses CSS into CSSOM tree
-
DOM + CSSOM → Render Tree
- Combines trees, excludes hidden elements
-
Layout
- Calculates exact position and size
-
Paint
- Fills in pixels (text, colors, images, shadows)
-
Composite
- Combines layers in correct order
🔴 Render-blocking:
- CSS blocks rendering (needs CSSOM before render tree)
- Synchronous JS blocks parsing
🟢 Non-blocking:
async/deferscripts- Images, fonts (don't block render)
<!-- Critical CSS inline -->
<style>/* Critical styles */</style>
<!-- Async load full CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
<!-- Defer non-critical JS -->
<script src="app.js" defer></script>Google's Core Web Vitals measure real user experience. They directly impact SEO and conversion. Understanding these metrics and how to improve them is essential for modern web development.
| Metric | Meaning | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | ≤ 2.5s | 2.5s - 4s | > 4s |
| INP | Interaction to Next Paint | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS | Cumulative Layout Shift | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
What: Time until the largest visible element loads
Measures: Loading performance
Common Causes:
- Slow server response (TTFB)
- Large, unoptimized images
- Render-blocking JavaScript/CSS
- Client-side rendering delays
How to Fix:
<!-- Optimize images -->
<img src="hero.webp" fetchpriority="high" alt="Hero">
<!-- Preload critical resources -->
<link rel="preload" as="image" href="hero.webp">
<!-- Use CDN -->
<img src="https://cdn.example.com/hero.webp">Server-side:
- Reduce TTFB with caching, CDN
- Use SSR or SSG for critical content
- Optimize database queries
What: Delay between user interaction and UI update (replaced FID in 2024)
Measures: Responsiveness
Common Causes:
- Long JavaScript tasks (>50ms)
- Heavy event handlers
- Expensive re-renders
- Main thread blocking
How to Fix:
// Break long tasks
async function processLargeList(items) {
for (let i = 0; i < items.length; i += 50) {
processBatch(items.slice(i, i + 50));
await scheduler.yield(); // Let browser breathe
}
}
// Use React 18 transitions
import { startTransition } from 'react';
function handleFilter(value) {
setInputValue(value); // Immediate
startTransition(() => {
setFilteredResults(filter(value)); // Deferred
});
}
// Debounce expensive operations
const debouncedSearch = useDeferredValue(searchQuery);Code splitting:
const HeavyComponent = lazy(() => import('./Heavy'));What: Unexpected layout movement while page loads
Measures: Visual stability
Common Causes:
- Images without dimensions
- Ads, embeds, iframes injected dynamically
- Web fonts causing FOUT/FOIT
- Animations that trigger layout
How to Fix:
<!-- Always set dimensions -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- Or use aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 16/9; width: 100%;">
<!-- Reserve space for ads -->
<div class="ad-slot" style="min-height: 250px;">
<!-- Ad loads here -->
</div>
<!-- Prevent font flash -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<style>
@font-face {
font-family: 'MyFont';
font-display: swap; /* or optional */
src: url('font.woff2');
}
</style>CSS transforms (don't trigger layout):
/* ❌ Causes layout shift */
.element {
top: 0;
transition: top 0.3s;
}
.element:hover {
top: -10px;
}
/* ✅ No layout shift */
.element {
transform: translateY(0);
transition: transform 0.3s;
}
.element:hover {
transform: translateY(-10px);
}TTFB – Time to First Byte
- Server response time
- Target: < 800ms
- Improve with: caching, CDN, database optimization
FCP – First Contentful Paint
- First pixel rendered
- Target: < 1.8s
- Improve with: inline critical CSS, reduce blocking resources
TBT – Total Blocking Time
- Lab metric (Lighthouse)
- Sum of blocking time from long tasks
- Correlates with INP
Field Data (Real Users):
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
// Send to analytics
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
navigator.sendBeacon('/analytics', body);
}
onLCP(sendToAnalytics);Lab Data (Testing):
- Lighthouse in Chrome DevTools
- Chrome UX Report (CrUX) - real user data
- PageSpeed Insights - combines lab + field data
- WebPageTest - detailed waterfall analysis
Tools:
- Chrome DevTools → Performance tab
- Google Search Console → Core Web Vitals report
- web.dev/measure
Core Web Vitals measure loading (LCP), interactivity (INP), and visual stability (CLS) based on real user experience.
Caching eliminates network requests entirely - the fastest request is no request. Service Workers enable sophisticated caching strategies and offline experiences.
// sw.js - Cache-first strategy
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/styles.css',
'/app.js',
'/logo.png'
]);
})
);
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(response => {
return response || fetch(e.request);
})
);
});1. Cache First
// Try cache, fallback to network
caches.match(request) || fetch(request)Use for: Static assets, images
2. Network First
// Try network, fallback to cache
fetch(request).catch(() => caches.match(request))Use for: API calls, dynamic content
3. Stale-While-Revalidate
const cached = await caches.match(request);
const fetching = fetch(request).then(res => {
cache.put(request, res.clone());
return res;
});
return cached || fetching;Use for: Balance freshness & speed
const db = await openDB('my-db', 1, {
upgrade(db) {
db.createObjectStore('posts');
}
});
await db.put('posts', data, 'post-123');
const post = await db.get('posts', 'post-123');"You can't improve what you don't measure." Performance monitoring should be continuous, not one-time.
Chrome DevTools → Performance Tab
- Record real interactions
- See flame charts, long tasks
- Identify bottlenecks
Lighthouse
- Automated audits
- Performance score + suggestions
- Built into Chrome DevTools
WebPageTest
- Test from different locations
- Connection throttling
- Filmstrip view, waterfall
React Profiler
import { Profiler } from 'react';
<Profiler id="App" onRender={callback}>
<App />
</Profiler>
function callback(id, phase, actualDuration) {
console.log(`${id} took ${actualDuration}ms`);
}Long Tasks (>50ms)
- Block user interactions
- Split into smaller chunks
JS Parse/Execute Time
- Large bundles slow startup
- Code split aggressively
Layout Shifts
- Measure CLS
- Set image dimensions
Network Waterfalls
- Identify blocking resources
- Check compression, caching
- Look for request chains
{
"budgets": [{
"resourceType": "script",
"budget": 300
}, {
"resourceType": "total",
"budget": 500
}]
}Set budgets, fail builds if exceeded.
Security is layered. No single measure is perfect, but combining multiple strategies creates robust protection. Always assume user input is malicious and verify everything server-side.
XSS allows attackers to inject malicious JavaScript into your site, running in the context of your domain with full access to cookies, localStorage, and the DOM.
Attack Vector: Attacker injects malicious JS into your site
1. Stored XSS
- Saved in database
- Affects all users who view it
- Example: Comment systems, user profiles
2. Reflected XSS
- Via URL/query parameters
- Affects single user (via phishing link)
- Example: Search results, error messages
3. DOM XSS
- Unsafe client-side JS manipulation
- Never reaches server
- Example:
innerHTML,eval()
// ❌ DANGEROUS
const userInput = "<img src=x onerror='alert(1)'>";
div.innerHTML = userInput; // XSS!
// ❌ DANGEROUS
const search = new URLSearchParams(location.search).get('q');
document.write(search); // XSS!X-Frame-Options Header
X-Frame-Options: DENY- DENY: Cannot be framed at all
- SAMEORIGIN: Only same-origin can frame
- ALLOW-FROM: Specific origin can frame (deprecated)
Content Security Policy (Modern)
Content-Security-Policy: frame-ancestors 'none';More flexible than X-Frame-Options.
CSP is a powerful security layer that restricts what resources can load and execute. It's your best defense against XSS attacks by whitelisting trusted sources.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
form-action 'self';| Directive | Purpose |
|---|---|
default-src |
Fallback for other directives |
script-src |
JavaScript sources |
style-src |
CSS sources |
img-src |
Image sources |
connect-src |
AJAX, WebSocket, fetch |
font-src |
Font sources |
object-src |
<object>, <embed> |
frame-ancestors |
Who can frame this page |
'self' # Same origin
'none' # Block everything
'unsafe-inline' # Allow inline scripts/styles (avoid!)
'unsafe-eval' # Allow eval() (avoid!)
'nonce-xyz123' # Specific inline script with nonce
'strict-dynamic' # Trust scripts loaded by trusted scripts<!-- Server generates random nonce -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'nonce-2726c7f26c'">
<!-- Only scripts with matching nonce execute -->
<script nonce="2726c7f26c">
console.log('This runs');
</script>
<script>
console.log('This is blocked');
</script>Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportLogs violations without blocking - perfect for testing.
HTTPS encrypts communication between browser and server, preventing eavesdropping and tampering. It's foundational to modern web security.
1. Encryption
- Prevents packet sniffing
- Protects sensitive data (passwords, credit cards)
2. Integrity
- Detects tampering
- Ensures data arrives unchanged
3. Authentication
- Verifies server identity
- Prevents impersonation
4. Modern APIs Require It
- Service Workers
- Geolocation
- Camera/microphone
- HTTP/2
HTTP Strict Transport Security (HSTS)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload- max-age: How long to remember (seconds)
- includeSubDomains: Apply to all subdomains
- preload: Include in browser's HSTS preload list
Redirect HTTP → HTTPS
// Server-side redirect
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});Browsers validate:
- Certificate is signed by trusted CA
- Certificate matches domain
- Certificate hasn't expired
- Certificate hasn't been revoked
Cookies are the primary authentication mechanism for web apps. Misconfigured cookies are a major security risk.
Set-Cookie:
sessionId=abc123;
HttpOnly;
Secure;
SameSite=Strict;
Max-Age=3600;
Path=/;
Domain=example.com| Flag | Purpose | Security Impact |
|---|---|---|
| HttpOnly | JavaScript can't read | Prevents XSS theft |
| Secure | HTTPS only | Prevents MITM |
| SameSite | Cross-site restrictions | Prevents CSRF |
| Max-Age | Expiration time | Limits exposure window |
| Path | URL scope | Principle of least privilege |
| Domain | Domain scope | Isolate subdomains |
# Strictest - never sent cross-site
SameSite=Strict
# Balanced - sent on top-level navigation
SameSite=Lax
# Permissive - always sent (requires Secure)
SameSite=None; SecureWhen to use:
- Strict: Session cookies
- Lax: Most use cases (default in modern browsers)
- None: Cross-site embeds (OAuth, payment widgets)
// Server-side (Express)
res.cookie('token', jwt, {
httpOnly: true, // No JS access
secure: true, // HTTPS only
sameSite: 'strict', // No CSRF
maxAge: 900000, // 15 minutes
path: '/',
signed: true // Tamper detection
});// ❌ Never trust client validation alone
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// ✅ Validate on backend too
app.post('/register', (req, res) => {
if (!isValidEmail(req.body.email)) {
return res.status(400).json({ error: 'Invalid email' });
}
// ... proceed
});// ❌ NEVER expose secrets in frontend
const API_KEY = 'sk_live_abc123'; // Visible in source!
// ✅ Use env vars at BUILD time only
const PUBLIC_KEY = process.env.NEXT_PUBLIC_STRIPE_KEY;
// ✅ Keep secrets server-side
// server.js
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);// ❌ Don't expose internal APIs
fetch('/admin/delete-all-users'); // Public endpoint!
// ✅ Verify authorization server-side
app.delete('/admin/users', authorize('admin'), (req, res) => {
// Check req.user.role
});
// ✅ Rate limiting
const rateLimit = require('express-rate-limit');
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
}));# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Use Dependabot or Renovate
# Regular updates prevent known exploits// helmet.js middleware (Express)
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-randomvalue'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny'
},
noSniff: true,
xssFilter: true
}));| ❌ Don't | ✅ Do |
|---|---|
| Store JWT in localStorage | Use HttpOnly cookies or memory |
| Trust client-side validation | Always validate server-side |
| Expose API keys in code | Use environment variables |
| Allow unlimited requests | Implement rate limiting |
Use eval() or Function() |
Parse JSON safely |
| Disable CORS without auth | Require authentication |
Q: Why is JWT in cookies safer than localStorage? A: HttpOnly cookies prevent JavaScript access, protecting against XSS token theft. localStorage is accessible to any script.
Q: Does CORS secure your backend? A: No, CORS is a browser-only restriction. Anyone can make requests from curl, Postman, or server-side code.
Q: Is React safe from XSS by default?
A: Yes, React escapes output by default. But dangerouslySetInnerHTML bypasses this protection.
Q: What's the best CSRF defense? A: Combination of SameSite cookies (Strict/Lax) + CSRF tokens for state-changing operations.
Q: How does CSP prevent XSS? A: By whitelisting trusted script sources and blocking inline scripts, CSP prevents injected malicious code from executing.
15% of the world's population lives with some form of disability. Accessibility isn't just ethical - it's:
- Legal requirement (ADA, Section 508)
- SEO benefit (semantic HTML helps crawlers)
- Better UX for everyone (keyboard nav, captions)
Web Content Accessibility Guidelines (WCAG) organized around 4 principles. Level AA compliance is the legal standard in most countries.
Users must be able to perceive content.
Text Alternatives
<!-- Images -->
<img src="chart.png" alt="Sales increased 20% in Q4 2024">
<!-- Decorative images -->
<img src="divider.png" alt="" role="presentation">
<!-- Icons with text -->
<button>
<svg aria-hidden="true">...</svg>
Delete
</button>Captions & Transcripts
<!-- Video with captions -->
<video controls>
<source src="video.mp4">
<track kind="captions" src="captions.vtt" srclang="en" label="English">
</video>
<!-- Audio transcript -->
<audio controls src="podcast.mp3"></audio>
<a href="transcript.txt">Read transcript</a>Color Contrast
- Normal text: 4.5:1 minimum
- Large text (18pt+): 3:1 minimum
- UI components: 3:1 minimum
/* ❌ Bad - insufficient contrast */
.text {
color: #999;
background: #fff; /* 2.8:1 */
}
/* ✅ Good - meets WCAG AA */
.text {
color: #595959;
background: #fff; /* 7:1 */
}Don't Rely on Color Alone
<!-- ❌ Color only -->
<span style="color: red;">Error</span>
<!-- ✅ Color + icon + text -->
<span class="error">
<svg aria-hidden="true">❌</svg>
Error: Invalid email format
</span>Users must be able to interact with UI.
Keyboard Accessibility
All interactive elements must be keyboard accessible:
Tab- move forwardShift + Tab- move backwardEnter/Space- activateEsc- close modals/menus- Arrow keys - navigate within components
/* ✅ Always show focus indicator */
button:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* ❌ NEVER do this */
*:focus {
outline: none;
}No Keyboard Traps
// ✅ Modal focus trap
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}Skip Links
<!-- Allow keyboard users to skip navigation -->
<a href="#main" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main">...</main>.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
}
.skip-link:focus {
top: 0;
}Avoid Flashing Content
- No more than 3 flashes per second
- Can trigger seizures
Users must understand content and interactions.
Clear Labels
<!-- ✅ Explicit label association -->
<label for="email">Email Address</label>
<input id="email" type="email" required>
<!-- ✅ Implicit label -->
<label>
Password
<input type="password" required>
</label>
<!-- ✅ ARIA label when visual label missing -->
<input type="search" aria-label="Search products">Helper Text
<label for="password">Password</label>
<input
id="password"
type="password"
aria-describedby="password-help"
required
>
<span id="password-help">
Must be at least 8 characters with 1 number
</span>Error Messages
<!-- ✅ Accessible error -->
<label for="email">Email</label>
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
>
<span id="email-error" role="alert">
Please enter a valid email address
</span>Predictable Navigation
- Consistent layout across pages
- Navigation in same place
- No unexpected focus changes
Content must work with assistive technologies.
Semantic HTML First
<!-- ✅ Semantic -->
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>© 2024</p>
</footer>Valid HTML
<!-- ❌ Invalid -->
<div>
<p>Text
</div>
<!-- ✅ Valid -->
<div>
<p>Text</p>
</div>ARIA When Needed
<!-- Loading state -->
<button aria-busy="true" aria-live="polite">
Loading...
</button>
<!-- Live region for dynamic content -->
<div aria-live="polite" aria-atomic="true">
3 items added to cart
</div>"No ARIA is better than bad ARIA."
If native HTML can do it, don't use ARIA.
<!-- ✅ Native button -->
<button>Click me</button>
<!-- ❌ ARIA button -->
<div role="button" tabindex="0" onclick="handleClick()">Click me</div>
<!-- ✅ Native navigation -->
<nav>
<a href="/about">About</a>
</nav>
<!-- ❌ ARIA navigation -->
<div role="navigation">
<span role="link" onclick="navigate('/about')">About</span>
</div>Custom Widgets
<!-- Tabs (no native HTML) -->
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel1">
Tab 1
</button>
<button role="tab" aria-selected="false" aria-controls="panel2">
Tab 2
</button>
</div>
<div id="panel1" role="tabpanel">Content 1</div>
<div id="panel2" role="tabpanel" hidden>Content 2</div>Dynamic Content
<!-- Alert -->
<div role="alert">Payment successful!</div>
<!-- Status update -->
<div role="status" aria-live="polite">
5 new messages
</div>Additional Context
<!-- Expanded state -->
<button aria-expanded="false" aria-controls="menu">
Menu
</button>
<ul id="menu" hidden>...</ul>
<!-- Current page -->
<nav>
<a href="/" aria-current="page">Home</a>
<a href="/about">About</a>
</nav>| Attribute | Purpose | Example |
|---|---|---|
aria-label |
Accessible name | <button aria-label="Close">×</button> |
aria-labelledby |
Reference to label | <div aria-labelledby="title"> |
aria-describedby |
Additional description | <input aria-describedby="help"> |
aria-hidden |
Hide from screen readers | <svg aria-hidden="true"> |
aria-live |
Announce updates | <div aria-live="polite"> |
aria-expanded |
Collapsible state | <button aria-expanded="true"> |
aria-current |
Current item | <a aria-current="page"> |
aria-invalid |
Validation state | <input aria-invalid="true"> |
Many users navigate exclusively via keyboard: power users, motor disabilities, screen reader users. Every interactive element must be keyboard accessible.
Tab Order
<!-- Natural tab order follows DOM order -->
<button>First</button>
<button>Second</button>
<button>Third</button>
<!-- ❌ Don't manipulate with tabindex > 0 -->
<button tabindex="3">Third</button>
<button tabindex="1">First</button>
<button tabindex="2">Second</button>
<!-- ✅ Use tabindex="-1" to exclude from tab order -->
<div tabindex="-1">Not tabbable but focusable</div>Focus Management
// ✅ Return focus after modal closes
function openModal() {
previousFocus = document.activeElement;
modal.show();
modal.querySelector('button').focus();
}
function closeModal() {
modal.hide();
previousFocus.focus(); // Restore focus
}Keyboard Event Handling
// ✅ Handle both click and keyboard
button.addEventListener('click', handleAction);
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});
// ✅ Or use <button> which does this automatically// Accessible dropdown
function Dropdown({ options }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e) => {
switch(e.key) {
case 'Enter':
case ' ':
e.preventDefault();
setIsOpen(!isOpen);
break;
case 'Escape':
setIsOpen(false);
break;
case 'ArrowDown':
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
break;
}
};
return (
<div>
<button
aria-expanded={isOpen}
aria-haspopup="listbox"
onKeyDown={handleKeyDown}
onClick={() => setIsOpen(!isOpen)}
>
Select option
</button>
{isOpen && (
<ul role="listbox">
{options.map((opt, i) => (
<li
key={opt.id}
role="option"
aria-selected={i === activeIndex}
>
{opt.label}
</li>
))}
</ul>
)}
</div>
);
}Color blindness affects 8% of men and 0.5% of women. Proper contrast ensures readability for everyone, including users with low vision or in bright sunlight.
- Normal text: 4.5:1
- Large text (18pt+ or 14pt+ bold): 3:1
- UI components & graphics: 3:1
- Chrome DevTools (Inspect → Accessibility)
- WebAIM Contrast Checker
- Stark plugin (Figma/Sketch)
/* ❌ Color alone conveys meaning */
.error {
color: #ff0000;
}
/* ✅ Color + icon + text */
.error {
color: #c00;
font-weight: 600;
}
.error::before {
content: '⚠️ ';
}<!-- ❌ Color-only legend -->
<div>
<span style="color: red;">■</span> Error
<span style="color: green;">■</span> Success
</div>
<!-- ✅ Icon + color + label -->
<div>
<span class="status-error">
<svg>❌</svg>
<span>Error</span>
</span>
<span class="status-success">
<svg>✅</svg>
<span>Success</span>
</span>
</div>/* ✅ High contrast focus */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* ✅ Different focus for dark backgrounds */
.dark-bg :focus-visible {
outline-color: #fff;
}
/* ❌ Removing outline without alternative */
:focus {
outline: none; /* DON'T DO THIS */
}// ❌ DIV button (inaccessible)
<div onClick={handleSave}>Save</div>
// ✅ Real button
<button onClick={handleSave}>Save</button>
// ✅ Button-styled link for navigation
<a href="/dashboard" className="button">Dashboard</a>function Modal({ isOpen, onClose, children }) {
const modalRef = useRef();
useEffect(() => {
if (isOpen) {
// Store previous focus
const previousFocus = document.activeElement;
// Focus modal
modalRef.current?.focus();
// Cleanup: restore focus
return () => previousFocus?.focus();
}
}, [isOpen]);
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
>
<h2 id="modal-title">Modal Title</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}function LoginForm() {
const [errors, setErrors] = useState({});
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email && (
<span id="email-error" role="alert">
{errors.email}
</span>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
aria-required="true"
aria-describedby="password-help"
/>
<span id="password-help">
At least 8 characters
</span>
</div>
<button type="submit">Log In</button>
</form>
);
}// ✅ Informative image
<img src="graph.png" alt="Revenue growth chart showing 25% increase in Q4" />
// ✅ Decorative image
<img src="divider.png" alt="" role="presentation" />
// ✅ Icon with text
<button>
<svg aria-hidden="true">
<TrashIcon />
</svg>
Delete
</button>
// ✅ Icon-only button
<button aria-label="Delete item">
<svg aria-hidden="true">
<TrashIcon />
</svg>
</button>function NotificationSystem() {
const [message, setMessage] = useState('');
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
{message}
</div>
);
}
// Usage
function handleSave() {
save();
setMessage('Changes saved successfully');
setTimeout(() => setMessage(''), 5000);
}Forms are the primary way users interact with web apps. Accessible forms are critical for usability.
<!-- ✅ Explicit association (preferred) -->
<label for="username">Username</label>
<input id="username" type="text">
<!-- ✅ Implicit association -->
<label>
Username
<input type="text">
</label>
<!-- ❌ No association -->
<label>Username</label>
<input type="text"><!-- ✅ Visual + programmatic -->
<label for="email">
Email <span aria-label="required">*</span>
</label>
<input id="email" type="email" required aria-required="true">
<!-- ✅ Alternative: text indication -->
<label for="email">Email (required)</label>
<input id="email" type="email" required>function SignupForm() {
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (name, value) => {
// Validation logic
};
return (
<form onSubmit={handleSubmit} noValidate>
{/* Error summary for screen readers */}
{Object.keys(errors).length > 0 && (
<div role="alert" aria-live="assertive">
<h2>Please fix the following errors:</h2>
<ul>
{Object.entries(errors).map(([field, message]) => (
<li key={field}>
<a href={`#${field}`}>{message}</a>
</li>
))}
</ul>
</div>
)}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : "email-help"}
onBlur={() => setTouched({ ...touched, email: true })}
/>
<span id="email-help">We'll never share your email</span>
{errors.email && touched.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
<button type="submit">Sign Up</button>
</form>
);
}<!-- ✅ Group related fields -->
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street</label>
<input id="street" type="text">
<label for="city">City</label>
<input id="city" type="text">
</fieldset>
<fieldset>
<legend>Payment Method</legend>
<label>
<input type="radio" name="payment" value="card">
Credit Card
</label>
<label>
<input type="radio" name="payment" value="paypal">
PayPal
</label>
</fieldset><!-- ✅ Help autofill and password managers -->
<input
type="email"
autocomplete="email"
id="email"
>
<input
type="password"
autocomplete="current-password"
id="current-password"
>
<input
type="password"
autocomplete="new-password"
id="new-password"
>
<input
type="text"
autocomplete="address-line1"
>Lighthouse (Chrome DevTools)
# Command line
lighthouse https://example.com --only-categories=accessibilityaxe DevTools
- Browser extension
- Catches 30-50% of issues
- Detailed remediation guidance
Pa11y
npm install -g pa11y
pa11y https://example.comJest + jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
expect.extend(toHaveNoViolations);
test('should have no accessibility violations', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});ESLint Plugin
{
"extends": ["plugin:jsx-a11y/recommended"],
"plugins": ["jsx-a11y"]
}Keyboard Navigation
- Unplug your mouse
- Navigate entire site with
Tab,Enter,Space,Esc, arrows - Check:
- Can you reach everything?
- Is focus visible?
- Is order logical?
- Any keyboard traps?
Screen Reader Testing
Windows: NVDA (free)
# Download from https://www.nvaccess.org/
# Ctrl + Alt + N to startmacOS: VoiceOver (built-in)
# Cmd + F5 to toggle
# VO + A to read page
# VO + Right Arrow to navigateCommon Screen Reader Commands:
| Action | NVDA | VoiceOver |
|---|---|---|
| Start/Stop | Ctrl + Alt + N | Cmd + F5 |
| Read all | Insert + ↓ | VO + A |
| Next item | ↓ | VO + → |
| Next heading | H | VO + Cmd + H |
| Next link | K | VO + Cmd + L |
| Forms mode | Insert + Space | Auto |
Color Contrast Checkers
- Chrome DevTools (Inspect element → Accessibility pane)
- WebAIM Contrast Checker
- Stark plugin (Figma)
- Colour Contrast Analyser (desktop app)
## Accessibility Testing Checklist
### Keyboard
- [ ] All interactive elements reachable via Tab
- [ ] Visible focus indicator on all elements
- [ ] Logical tab order (follows visual layout)
- [ ] No keyboard traps
- [ ] Esc closes modals/dropdowns
- [ ] Skip navigation link works
### Screen Reader
- [ ] Page has meaningful title
- [ ] Headings create logical outline
- [ ] All images have alt text (or alt="" for decorative)
- [ ] Form labels properly associated
- [ ] Error messages announced
- [ ] Dynamic content updates announced
- [ ] ARIA attributes used correctly
### Visual
- [ ] Text contrast meets WCAG AA (4.5:1)
- [ ] Focus indicators meet contrast (3:1)
- [ ] Color not sole means of conveying info
- [ ] Text resizes to 200% without breaking
- [ ] No information lost in mobile view
### Content
- [ ] Link text descriptive ("Read more about accessibility" not "Click here")
- [ ] Headings in correct order (h1 → h2 → h3)
- [ ] Language attribute set (<html lang="en">)
- [ ] Page has unique, descriptive title
### Forms
- [ ] All inputs have labels
- [ ] Required fields marked (visually + programmatically)
- [ ] Error messages clear and associated with fields
- [ ] Success messages announced
- [ ] Autocomplete attributes used where appropriate
### Media
- [ ] Videos have captions
- [ ] Audio has transcripts
- [ ] No autoplay (or easy to pause)
- [ ] No content flashes more than 3x per secondQ: What's the biggest performance killer? A: Too much JavaScript. It must be downloaded, parsed, compiled, and executed - all blocking the main thread.
Q: What's the fastest performance win? A: Code splitting + image optimization. Lazy load what you don't need immediately.
Q: Why use transform over top/left? A: Transform only triggers compositing, while top/left triggers full layout recalculation (reflow).
Q: Why use React.memo? A: Prevents unnecessary re-renders by memoizing the component, only re-rendering when props change.
Q: Why use a CDN? A: Lower latency (geographically closer to users) + edge caching + reduces load on origin server.
Q: What are Core Web Vitals? A: Google's user-centric performance metrics: LCP (loading), INP (interactivity), CLS (visual stability).
Q: Difference between defer and async? A: Both download in parallel, but defer executes after HTML parsing while async executes immediately when downloaded.
Q: What's the Critical Rendering Path? A: HTML→DOM, CSS→CSSOM, DOM+CSSOM→Render Tree, Layout, Paint, Composite.
Q: How to reduce bundle size? A: Tree-shaking, code splitting, dynamic imports, remove unused dependencies, compression (Brotli/Gzip).
Q: What's lazy loading? A: Deferring load of non-critical resources until they're needed (images, routes, components).
Q: What is XSS? A: Cross-Site Scripting - attacker injects malicious JavaScript into your site, executing in users' browsers.
Q: What is CSRF? A: Cross-Site Request Forgery - attacker tricks users into making unwanted authenticated requests.
Q: Why is JWT in cookies safer than localStorage? A: HttpOnly cookies prevent JavaScript access, protecting against XSS token theft.
Q: Does CORS secure your backend? A: No, CORS is a browser-only restriction. Server-to-server requests bypass CORS entirely.
Q: Is React safe from XSS by default?
A: Yes, React escapes output automatically. But dangerouslySetInnerHTML bypasses this protection.
Q: Best CSRF defense? A: SameSite cookies (Strict/Lax) + CSRF tokens for state-changing operations.
Q: What does CSP do? A: Content Security Policy whitelists trusted sources for scripts, styles, and other resources, blocking injected malicious code.
Q: Why HTTPS? A: Encryption (prevents eavesdropping), integrity (prevents tampering), authentication (verifies server identity).
Q: What are secure cookie flags? A: HttpOnly (no JS access), Secure (HTTPS only), SameSite (CSRF protection).
Q: What's the difference between authentication and authorization? A: Authentication is "who are you?" (login), Authorization is "what can you access?" (permissions).
Q: What is accessibility? A: Making web content usable by everyone, including people with disabilities (visual, auditory, motor, cognitive).
Q: What are the WCAG principles? A: POUR - Perceivable, Operable, Understandable, Robust.
Q: Semantic HTML vs ARIA? A: Always use semantic HTML first. ARIA is only for custom widgets where native HTML doesn't exist.
Q: Why is keyboard accessibility important? A: Many users can't use a mouse: power users, motor disabilities, screen reader users.
Q: What's the golden rule of ARIA? A: "No ARIA is better than bad ARIA." If native HTML can do it, don't use ARIA.
Q: What color contrast ratio is required? A: WCAG AA requires 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold).
Q: Why not remove focus outlines? A: Keyboard users need visible focus indicators to know where they are on the page.
Q: What is alt text for? A: Describes images for screen reader users and when images fail to load. Use alt="" for decorative images.
Q: What's aria-label vs aria-labelledby? A: aria-label provides the label directly. aria-labelledby references another element's ID.
Q: How to make a modal accessible? A: Trap focus inside modal, restore focus on close, aria-modal="true", close on Esc, focus first element on open.
Network (biggest wins)
↓
JavaScript (parse/execute cost)
↓
React (render optimization)
↓
CSS (layout triggers)
↓
Images (lazy loading)
↓
Caching (offline)
One-sentence summary: Minimize network requests, reduce JavaScript, optimize renders, avoid layout thrashing.
XSS = Execute malicious JS
CSRF = Abuse trust
CORS = Browser-only rule
CSP = Whitelist trusted sources
Defense layers:
- Input validation (client + server)
- Output escaping
- CSP headers
- Secure cookies (HttpOnly, Secure, SameSite)
- HTTPS everywhere
Perceivable → Can users see/hear content?
↓
Operable → Can users interact?
↓
Understandable → Can users comprehend?
↓
Robust → Does it work with assistive tech?
One-sentence summary: Semantic HTML first, keyboard accessible, sufficient contrast, screen reader friendly.
Q: How would you optimize a slow React app?
A: Systematic approach:
- Profile first - React DevTools Profiler, identify slow components
- Prevent re-renders - React.memo, useCallback, useMemo
- Code split - Lazy load routes and heavy components
- Virtualize lists - react-window for long lists
- Optimize images - WebP, lazy loading, responsive images
- Reduce bundle - Tree-shake, remove unused deps
- Measure Core Web Vitals - Focus on LCP, INP, CLS
Q: Walk me through optimizing a page with LCP of 5 seconds.
A:
- Identify LCP element - Use Lighthouse or Chrome DevTools
- If it's an image:
- Optimize format (WebP/AVIF)
- Use CDN
- Preload:
<link rel="preload" as="image" href="hero.jpg"> - Add fetchpriority="high"
- If it's text:
- Inline critical CSS
- Preconnect to font sources
- Use font-display: swap
- Reduce TTFB:
- Server-side rendering
- Edge caching
- Database optimization
- Remove render-blocking resources:
- Defer non-critical JS
- Async CSS loading
Q: Explain how you'd implement infinite scroll with good performance.
A:
import { useEffect, useRef, useState } from 'react';
import { FixedSizeList } from 'react-window';
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const observer = useRef();
// Intersection Observer for detecting scroll bottom
const lastItemRef = useCallback(node => {
if (loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
setPage(prev => prev + 1);
}
});
if (node) observer.current.observe(node);
}, [loading]);
// Fetch more data
useEffect(() => {
setLoading(true);
fetchItems(page).then(newItems => {
setItems(prev => [...prev, ...newItems]);
setLoading(false);
});
}, [page]);
// Virtualize for performance
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={80}
width="100%"
>
{({ index, style }) => (
<div
style={style}
ref={index === items.length - 1 ? lastItemRef : null}
>
{items[index].title}
</div>
)}
</FixedSizeList>
);
}Key optimizations:
- Virtualization (only render visible items)
- Intersection Observer (native, performant)
- Avoid layout thrashing
- Debounce scroll events if needed
Q: How would you secure a user authentication system?
A: Multi-layered approach:
-
Password Security:
- Hash with bcrypt/Argon2 (never store plaintext)
- Enforce strong password requirements
- Rate limit login attempts
-
Token Management:
- Short-lived access tokens (15min) in memory
- Long-lived refresh tokens in HttpOnly cookies
- Rotate refresh tokens on use
-
Transport Security:
- HTTPS everywhere
- HSTS headers
- Secure cookie flags (HttpOnly, Secure, SameSite)
-
CSRF Protection:
- SameSite=Strict cookies
- CSRF tokens for state-changing operations
-
XSS Prevention:
- CSP headers
- Escape output (React does by default)
- Sanitize any dangerouslySetInnerHTML
// Example implementation
app.post('/login', rateLimiter, async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user || !await bcrypt.compare(req.body.password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = jwt.sign({ userId: user.id }, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: '7d' });
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
});Q: A user reports they can see other users' data. How do you investigate?
A: Systematic approach:
-
Immediate action:
- Isolate affected endpoint
- Review recent deployments
- Check logs for unauthorized access
-
Common causes:
- Missing authorization checks
- SQL injection
- IDOR (Insecure Direct Object Reference)
- Broken access control
-
Investigation:
// ❌ Vulnerable code
app.get('/api/user/:id', (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // No authorization check!
});
// ✅ Fixed code
app.get('/api/user/:id', authenticate, async (req, res) => {
const user = await User.findById(req.params.id);
// Verify requesting user owns this data
if (user.id !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(user);
});- Prevention:
- Always verify authorization server-side
- Principle of least privilege
- Audit logs
- Regular security testing
Q: How would you make a complex data table accessible?
A:
<table>
<caption>
Quarterly Sales Report
<details>
<summary>Table description</summary>
Shows sales by region and product for Q4 2024
</details>
</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Product</th>
<th scope="col">Revenue</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>Widget A</td>
<td>$125,000</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row" colspan="2">Total</th>
<td>$500,000</td>
</tr>
</tfoot>
</table>Key features:
<caption>describes table purposescope="col"for column headersscope="row"for row headers- Sortable columns announce state
- Keyboard navigation (arrow keys)
- For complex tables:
headersattribute
For large tables:
- Pagination or virtual scrolling
- Filter/search functionality
- Sticky headers
- Export to CSV option
Q: User complains your site doesn't work with keyboard. How do you debug?
A: Systematic debugging:
-
Reproduce issue:
- Unplug mouse
- Navigate with Tab only
- Document what fails
-
Common issues:
// ❌ Problem 1: Div buttons
<div onClick={handleClick}>Click me</div>
// ✅ Fix: Use real button
<button onClick={handleClick}>Click me</button>
// ❌ Problem 2: No visible focus
button:focus { outline: none; }
// ✅ Fix: Clear focus indicator
button:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
// ❌ Problem 3: Modal keyboard trap
function Modal({ isOpen, onClose }) {
return <div>{children}</div>; // Focus escapes modal
}
// ✅ Fix: Trap focus
function Modal({ isOpen, onClose }) {
const modalRef = useRef();
useEffect(() => {
if (!isOpen) return;
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const trapFocus = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
};
modalRef.current.addEventListener('keydown', trapFocus);
firstElement.focus();
return () => {
modalRef.current?.removeEventListener('keydown', trapFocus);
};
}, [isOpen]);
return (
<div ref={modalRef} role="dialog" aria-modal="true">
{children}
</div>
);
}- Testing checklist:
- All interactive elements reachable
- Visible focus indicator
- Logical tab order
- Escape closes overlays
- No keyboard traps
- Custom widgets have proper keyboard handling
{
"budget": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "stylesheet",
"budget": 50
},
{
"resourceType": "image",
"budget": 400
},
{
"resourceType": "total",
"budget": 800
}
],
"metrics": [
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "first-contentful-paint",
"budget": 1500
}
]
}Enforce in CI/CD:
# Lighthouse CI
lighthouse-ci --budget-path=budget.json --fail-on-violation- web.dev/metrics - Core Web Vitals guide
- WebPageTest - Performance testing
- React DevTools Profiler - React performance
- OWASP Top 10 - Common vulnerabilities
- MDN Web Security - Security fundamentals
- web.dev/secure - HTTPS and security
- WCAG 2.1 - Accessibility guidelines
- A11y Project - Accessibility checklist
- WebAIM - Accessibility resources and tools
- axe DevTools - Accessibility testing
Goal: Fast, smooth user experience
- Minimize network (CDN, compression, caching)
- Optimize JavaScript (code splitting, defer)
- Efficient React (memo, virtualization)
- Measure Core Web Vitals (LCP, INP, CLS)
Goal: Protect users and data
- Prevent XSS (escape output, CSP)
- Prevent CSRF (SameSite cookies, tokens)
- Secure auth (HTTPS, HttpOnly cookies, JWT rotation)
- Defense in depth (multiple layers)
Goal: Usable by everyone
- Semantic HTML first
- Keyboard accessible
- Screen reader friendly
- Sufficient contrast
- POUR principles (Perceivable, Operable, Understandable, Robust)