- Core Web Vitals
- Critical Rendering Path (CRP)
- Performance Metrics
- Caching Strategies
- Asset Optimization
- Bundle Optimization
- React-Specific Optimizations
- Network Optimization
- Runtime Performance
- Web Security
- Monitoring & Tools
What: Google's key metrics for measuring user experience on the web.
What: Measures loading performance - when the largest content element becomes visible.
Target: < 2.5 seconds
What's Measured:
<img>elements<video>elements- Block-level elements with background images
- Text blocks
How to Improve:
// Preload critical images
<link rel="preload" as="image" href="hero.jpg">
// Use modern image formats
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="description">
</picture>
// Priority hints
<img src="hero.jpg" fetchpriority="high">
// Lazy load below-fold images
<img src="image.jpg" loading="lazy">Common Issues:
- Slow server response times
- Render-blocking JavaScript/CSS
- Large images not optimized
- Client-side rendering delays
What:
- FID (deprecated): Time from first user interaction to browser response
- INP (new): Responsiveness of all user interactions
Target:
- FID: < 100ms
- INP: < 200ms
How to Improve:
// 1. Code splitting - load only what's needed
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// 2. Break up long tasks
async function processLargeData(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield to main thread every 50 items
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
// 3. Use Web Workers for heavy computation
const worker = new Worker('heavy-task.js');
worker.postMessage(data);
// 4. Debounce/throttle event handlers
const handleScroll = debounce(() => {
// Handle scroll
}, 100);
// 5. Use requestIdleCallback
requestIdleCallback(() => {
// Non-critical work
});Common Issues:
- Large JavaScript bundles blocking main thread
- Long-running event handlers
- Heavy computations on main thread
- Third-party scripts
What: Measures visual stability - unexpected layout shifts during page load.
Target: < 0.1
How to Improve:
/* 1. Reserve space for images */
img {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
/* 2. Reserve space for ads/embeds */
.ad-container {
min-height: 250px;
}
/* 3. Use transform for animations (not top/left) */
.animated {
transform: translateY(10px); /* Good */
/* top: 10px; Bad - causes layout shift */
}<!-- Reserve space with aspect ratio -->
<img src="image.jpg" width="800" height="600" alt="description">
<!-- Use skeleton screens -->
<div class="skeleton" style="width: 100%; height: 200px;"></div>Common Issues:
- Images without dimensions
- Ads/embeds without reserved space
- Fonts causing FOIT/FOUT
- Dynamic content insertion above viewport
Target: < 800ms
- Server response time
- CDN usage
- Database optimization
Target: < 1.8s
- When first DOM content renders
- Critical CSS inline
- Remove render-blocking resources
What: The sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on screen.
1. DOM Construction (HTML → DOM Tree)
↓
2. CSSOM Construction (CSS → CSSOM Tree)
↓
3. Render Tree (DOM + CSSOM → Render Tree)
↓
4. Layout (Calculate positions/sizes)
↓
5. Paint (Draw pixels)
↓
6. Composite (Layer composition)
<!-- Bad: Render-blocking CSS -->
<link rel="stylesheet" href="styles.css">
<!-- Good: Critical CSS inline, rest async -->
<style>
/* Critical above-fold CSS */
.header { /* ... */ }
</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>// Minify and compress
// Before: 100KB uncompressed
// After: 25KB minified + gzipped
// Use tree-shaking
import { specificFunction } from 'library'; // Good
// import * as library from 'library'; // Bad<!-- Defer non-critical JavaScript -->
<script src="analytics.js" defer></script>
<script src="non-critical.js" async></script>
<!-- Critical JS inline -->
<script>
// Critical initialization code
</script><!-- Preload: High priority, fetch ASAP -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<!-- Prefetch: Low priority, for next navigation -->
<link rel="prefetch" href="next-page.js">
<!-- Preconnect: Establish early connection -->
<link rel="preconnect" href="https://api.example.com">
<!-- DNS-prefetch: Resolve DNS early -->
<link rel="dns-prefetch" href="https://analytics.com">
<!-- Script loading -->
<script src="app.js" defer></script> <!-- Executes after DOM -->
<script src="app.js" async></script> <!-- Executes ASAP -->
<script src="app.js"></script> <!-- Blocks parsing -->// Performance API
const perfData = performance.getEntriesByType('navigation')[0];
console.log({
// DNS lookup time
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
// TCP connection time
tcp: perfData.connectEnd - perfData.connectStart,
// Time to First Byte
ttfb: perfData.responseStart - perfData.requestStart,
// DOM processing
domProcessing: perfData.domComplete - perfData.domLoading,
// Total page load
pageLoad: perfData.loadEventEnd - perfData.navigationStart
});
// Measure custom metrics
performance.mark('componentMount-start');
// ... component code ...
performance.mark('componentMount-end');
performance.measure('componentMount', 'componentMount-start', 'componentMount-end');
// Get measurements
const measures = performance.getEntriesByName('componentMount');
console.log(measures[0].duration);// Using web-vitals library
import { onCLS, onFID, onLCP, onINP } from 'web-vitals';
onLCP(console.log);
onFID(console.log);
onCLS(console.log);
onINP(console.log);
// Send to analytics
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', body);
} else {
fetch('/analytics', { body, method: 'POST', keepalive: true });
}
}
onCLS(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);# nginx configuration
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~* \.(html)$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}Cache-Control Directives:
# Static assets with hash (aggressive caching)
Cache-Control: public, max-age=31536000, immutable
# HTML (no caching)
Cache-Control: no-cache, must-revalidate
# API responses (short-lived cache)
Cache-Control: private, max-age=300
# No caching at all
Cache-Control: no-store// sw.js - Service Worker
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
'/',
'/styles.css',
'/app.js',
'/logo.png'
];
// Install - cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});
// Fetch - cache strategies
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache first, fallback to network
return response || fetch(event.request);
})
);
});
// Activate - clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});Common Caching Strategies:
// 1. Cache First (good for static assets)
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}
// 2. Network First (good for dynamic content)
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
return caches.match(request);
}
}
// 3. Stale While Revalidate (best UX for most cases)
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}// Simple memoization
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
};
const expensiveCalculation = memoize((n) => {
// Heavy computation
return n * 2;
});
// React Query / SWR for API caching
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000 // 10 minutes
});
}// Cloudflare Workers example
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const cache = caches.default;
// Check cache
let response = await cache.match(request);
if (!response) {
// Fetch from origin
response = await fetch(request);
// Cache for 1 hour
response = new Response(response.body, response);
response.headers.set('Cache-Control', 'max-age=3600');
event.waitUntil(cache.put(request, response.clone()));
}
return response;
}// Use modern formats
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="description">
</picture>
// Responsive images
<img
srcset="
small.jpg 300w,
medium.jpg 768w,
large.jpg 1024w
"
sizes="
(max-width: 300px) 300px,
(max-width: 768px) 768px,
1024px
"
src="medium.jpg"
alt="description"
>
// Lazy loading
<img src="image.jpg" loading="lazy" alt="description">
// Next.js Image component (automatic optimization)
import Image from 'next/image';
<Image
src="/hero.jpg"
width={800}
height={600}
priority // For LCP image
alt="Hero"
/>Image Optimization Checklist:
- ✅ Use WebP/AVIF formats (60-80% smaller than JPEG)
- ✅ Compress images (TinyPNG, ImageOptim)
- ✅ Serve responsive images with
srcset - ✅ Lazy load below-fold images
- ✅ Use CDN for image delivery
- ✅ Set explicit dimensions to prevent CLS
- ✅ Use blur-up placeholders
/* 1. Font-display strategy */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
/* optional: Invisible for 100ms, then fallback */
/* block: Invisible for 3s, then fallback */
/* fallback: Invisible for 100ms, swap for 3s */
}
/* 2. Subset fonts (include only needed characters) */
/* Use tools like glyphhanger */
/* 3. Preload critical fonts */<link
rel="preload"
href="/fonts/custom.woff2"
as="font"
type="font/woff2"
crossorigin
>
<!-- Use system fonts as fallback -->
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
}
</style>// 1. Critical CSS inline
// Extract above-fold CSS and inline in <head>
// 2. Remove unused CSS
// Use PurgeCSS or built-in tools
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// PurgeCSS removes unused styles
}
// 3. CSS-in-JS optimization (styled-components)
import { ServerStyleSheet } from 'styled-components';
// Extract critical CSS on server
const sheet = new ServerStyleSheet();
const html = renderToString(sheet.collectStyles(<App />));
const styleTags = sheet.getStyleTags();
// 4. Minify CSS
// Use cssnano or similar// 1. Minification (Terser)
// Removes whitespace, shortens variable names
// 2. Tree-shaking (remove unused code)
// Use ES modules, not CommonJS
import { specificFunction } from 'library'; // ✓ Tree-shakeable
const library = require('library'); // ✗ Not tree-shakeable
// 3. Dead code elimination
// Build tools remove unreachable code
if (false) {
// This code is eliminated in production
}// Route-based splitting (React Router)
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
function Analytics() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
);
}
// Dynamic imports
button.addEventListener('click', async () => {
const module = await import('./heavyModule.js');
module.doSomething();
});// webpack.config.js
module.exports = {
optimization: {
// Split vendor code
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
},
// Runtime chunk
runtimeChunk: 'single',
// Minimize
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.logs
}
}
})
]
},
// Performance budgets
performance: {
maxAssetSize: 244000, // 244 KiB
maxEntrypointSize: 244000,
hints: 'warning'
}
};// vite.config.js
export default {
build: {
// Manual chunks
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
'ui': ['@mui/material']
}
}
},
// Chunk size warnings
chunkSizeWarningLimit: 500,
// Minification
minify: 'terser',
terserOptions: {
compress: {
drop_console: true
}
}
}
};# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to webpack config
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
# For Vite
npm install --save-dev rollup-plugin-visualizer
# vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [visualizer()]
};// Memoization
import { memo, useMemo, useCallback } from 'react';
// Prevent re-renders
const ExpensiveComponent = memo(({ data }) => {
return <div>{/* Complex rendering */}</div>;
});
// Memoize computed values
function DataTable({ items }) {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.value - b.value);
}, [items]);
return <Table data={sortedItems} />;
}
// Memoize callbacks
function Parent() {
const handleClick = useCallback(() => {
// Handle click
}, []); // Deps array
return <Child onClick={handleClick} />;
}// react-window for large lists
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// react-virtuoso (more features)
import { Virtuoso } from 'react-virtuoso';
<Virtuoso
data={items}
itemContent={(index, item) => <ItemComponent item={item} />}
/>import { useTransition, useDeferredValue, startTransition } from 'react';
// useTransition - mark updates as non-urgent
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // Urgent
startTransition(() => {
// Non-urgent update
fetchResults(value);
});
};
return (
<>
<input onChange={handleChange} />
{isPending && <Spinner />}
</>
);
}
// useDeferredValue - defer expensive updates
function ProductList({ query }) {
const deferredQuery = useDeferredValue(query);
// This re-renders with old value while new one is pending
const results = useMemo(() =>
searchProducts(deferredQuery),
[deferredQuery]
);
return <List items={results} />;
}// Context splitting (avoid unnecessary re-renders)
// Bad - everything re-renders on any state change
const AppContext = createContext();
// Good - split into separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const DataContext = createContext();
// Use selectors with Redux/Zustand
import { useSelector } from 'react-redux';
function UserProfile() {
// Only re-renders when user.name changes
const userName = useSelector(state => state.user.name);
return <div>{userName}</div>;
}
// Zustand with selectors
import create from 'zustand';
const useStore = create((set) => ({
user: { name: '', email: '' },
theme: 'light',
updateUser: (user) => set({ user })
}));
function Component() {
// Only subscribes to user.name
const userName = useStore(state => state.user.name);
}// React Suspense for data fetching
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<Loading />}>
<UserProfile />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</Suspense>
);
}
// Streaming SSR (Next.js)
// app/page.js
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<Footer />
</>
);
}
// Header and Footer stream immediately,
// SlowComponent streams when ready- Multiplexing: Multiple requests over single connection
- Header compression: HPACK compression
- Server push: Proactively send resources
# Enable HTTP/2
listen 443 ssl http2;
# HTTP/3 (QUIC)
listen 443 quic reuseport;<!-- DNS Prefetch -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- Prefetch (for next page) -->
<link rel="prefetch" href="/next-page.js">
<!-- Preload (current page critical) -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/font.woff2" as="font" crossorigin>
<!-- Modulepreload (for ES modules) -->
<link rel="modulepreload" href="/app.js">// Enable gzip/brotli compression
// nginx
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
# Brotli (better compression)
brotli on;
brotli_types text/plain text/css application/json application/javascript;// 1. GraphQL - fetch only needed data
const QUERY = gql`
query User($id: ID!) {
user(id: $id) {
name
email
# Only fields you need
}
}
`;
// 2. Pagination
const PAGE_SIZE = 20;
fetch(`/api/users?page=1&limit=${PAGE_SIZE}`);
// 3. Debounce API calls
import debounce from 'lodash/debounce';
const searchAPI = debounce(async (query) => {
const results = await fetch(`/search?q=${query}`);
return results.json();
}, 300);
// 4. Request deduplication
const cache = new Map();
async function fetchWithCache(url) {
if (cache.has(url)) {
return cache.get(url);
}
const promise = fetch(url).then(r => r.json());
cache.set(url, promise);
return promise;
}
// 5. Parallel requests
const [users, posts, comments] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);// Bad - forces multiple reflows
for (let i = 0; i < elements.length; i++) {
const height = elements[i].offsetHeight; // Read
elements[i].style.height = height + 10 + 'px'; // Write
}
// Good - batch reads and writes
const heights = elements.map(el => el.offsetHeight); // Batch reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // Batch writes
});
// Use requestAnimationFrame for visual changes
function updateAnimation() {
element.style.transform = `translateX(${x}px)`;
requestAnimationFrame(updateAnimation);
}// Event delegation
document.querySelector('#list').addEventListener('click', (e) => {
if (e.target.matches('.item')) {
// Handle item click
}
});
// Passive event listeners
element.addEventListener('touchstart', handler, { passive: true });
// Remove event listeners
useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);// Lazy load images
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
// Infinite scroll
const loadMore = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
fetchNextPage();
}
});
loadMore.observe(document.querySelector('#load-more-trigger'));// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// React hook for Web Worker
function useWorker(workerFunction) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(
URL.createObjectURL(
new Blob([`(${workerFunction})()`])
)
);
worker.onmessage = (e) => setResult(e.data);
return () => worker.terminate();
}, []);
return result;
}What: Injection of malicious scripts into trusted websites.
Types:
- Stored XSS: Malicious script stored in database
- Reflected XSS: Script in URL/input reflected back
- DOM-based XSS: Client-side script manipulation
// ❌ BAD - Vulnerable to XSS
function displayUserInput(input) {
document.getElementById('output').innerHTML = input;
// User input: <img src=x onerror=alert('XSS')>
}
// ✅ GOOD - Use textContent
function displayUserInput(input) {
document.getElementById('output').textContent = input;
}
// ✅ React automatically escapes
function UserComment({ text }) {
return <div>{text}</div>; // Automatically escaped
}
// ❌ BAD - dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ GOOD - Sanitize before rendering
import DOMPurify from 'dompurify';
function SafeHTML({ html }) {
const sanitized = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
// Server-side sanitization (Node.js)
const xss = require('xss');
const clean = xss(userInput);
// Escape user input in templates
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}Best Practices:
- ✅ Never trust user input
- ✅ Sanitize/escape all user-generated content
- ✅ Use Content Security Policy (CSP)
- ✅ Use
textContentinstead ofinnerHTML - ✅ Validate input on both client and server
- ✅ Use HttpOnly cookies for sensitive data
What: Attacker tricks user into executing unwanted actions on authenticated site.
How it works:
<!-- Malicious site -->
<img src="https://bank.com/transfer?to=attacker&amount=1000">
<!-- If user is logged into bank.com, this executes -->// 1. CSRF Tokens (Synchronizer Token Pattern)
// Backend generates unique token per session
app.get('/form', (req, res) => {
const csrfToken = generateToken();
req.session.csrfToken = csrfToken;
res.render('form', { csrfToken });
});
// Frontend includes token in requests
<form method="POST" action="/transfer">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<input name="amount" />
<button type="submit">Transfer</button>
</form>
// Backend validates token
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('Invalid CSRF token');
}
// Process transfer
});
// 2. SameSite Cookies
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'strict' // or 'lax'
});
// 3. Custom Headers (for AJAX)
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: 100 })
});
// Server validates custom header
if (!req.headers['x-csrf-token']) {
return res.status(403).send('Missing CSRF token');
}
// 4. Double Submit Cookie Pattern
// Set CSRF token in cookie AND require it in request body
res.cookie('csrf-token', token);
// Client sends same token in header
fetch('/api/data', {
headers: { 'X-CSRF-Token': getCookie('csrf-token') }
});React + CSRF:
// Use axios interceptor
import axios from 'axios';
axios.interceptors.request.use(config => {
const token = document.querySelector('meta[name="csrf-token"]').content;
config.headers['X-CSRF-Token'] = token;
return config;
});
// Or with fetch wrapper
function secureFetch(url, options = {}) {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
return fetch(url, {
...options,
headers: {
...options.headers,
'X-CSRF-Token': csrfToken
}
});
}What: HTTP header that controls which resources browser can load, preventing XSS.
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com// HTML meta tag (less recommended)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
// Express.js
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://trusted.cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
}));
// Next.js (next.config.js)
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline';"
}]
}];
}
};CSP Directives:
default-src 'self' # Default policy for all resources
script-src 'self' cdn.com # JavaScript sources
style-src 'self' 'unsafe-inline' # CSS sources
img-src 'self' data: https: # Image sources
connect-src 'self' api.com # AJAX, WebSocket sources
font-src 'self' fonts.com # Font sources
object-src 'none' # Plugins (Flash, etc.)
frame-ancestors 'none' # Who can embed in iframe
upgrade-insecure-requests # Upgrade HTTP to HTTPS
Nonce-based CSP (best practice):
// Server generates unique nonce
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
// Include nonce in script tags
<script nonce="${nonce}">
// Inline script allowed
</script>
// React with Next.js
import Script from 'next/script';
<Script nonce={nonce}>
{`console.log('Secure inline script');`}
</Script>CSP Reporting:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
# Browser sends violations to /csp-reportWhat: Tricking users into clicking something different from what they perceive.
Attack Example:
<!-- Attacker's site -->
<iframe src="https://bank.com/transfer" style="opacity: 0; position: absolute;"></iframe>
<button>Click for Free Prize!</button>
<!-- User clicks button, actually clicks hidden iframe -->// 1. X-Frame-Options Header (older)
res.setHeader('X-Frame-Options', 'DENY'); // Cannot be framed
res.setHeader('X-Frame-Options', 'SAMEORIGIN'); // Only same origin
// 2. CSP frame-ancestors (modern, preferred)
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'"); // DENY
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'"); // SAMEORIGIN
res.setHeader('Content-Security-Policy', "frame-ancestors 'self' https://trusted.com");
// 3. JavaScript Frame Busting (fallback)
if (window.top !== window.self) {
window.top.location = window.self.location;
}
// Express with Helmet
const helmet = require('helmet');
app.use(helmet({
frameguard: { action: 'deny' }
}));
// Next.js headers
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'none'"
}
]
}];
}
};What: Inserting malicious SQL code through user input.
// ❌ BAD - Vulnerable to SQL injection
app.get('/user', (req, res) => {
const userId = req.query.id;
db.query(`SELECT * FROM users WHERE id = ${userId}`, (err, result) => {
// Attacker sends: ?id=1 OR 1=1
// Query becomes: SELECT * FROM users WHERE id = 1 OR 1=1
});
});
// ✅ GOOD - Parameterized queries
app.get('/user', (req, res) => {
const userId = req.query.id;
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, result) => {
// Safe - parameters are escaped
});
});
// With PostgreSQL (node-postgres)
const result = await pool.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// With ORM (Sequelize)
const user = await User.findOne({
where: { email: email }
});
// With Prisma
const user = await prisma.user.findUnique({
where: { email: email }
});Best Practices:
- ✅ Always use parameterized queries/prepared statements
- ✅ Use ORMs (Sequelize, Prisma, TypeORM)
- ✅ Validate and sanitize input
- ✅ Use least privilege for database users
- ✅ Never concatenate user input into SQL
What:
- DoS: Single source overwhelming server
- DDoS: Multiple sources (distributed) overwhelming server
// 1. Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// Strict rate limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.post('/login', authLimiter, loginHandler);
// 2. Request Size Limits
const express = require('express');
app.use(express.json({ limit: '10kb' })); // Limit JSON body size
app.use(express.urlencoded({ limit: '10kb', extended: true }));
// 3. Timeout Middleware
const timeout = require('connect-timeout');
app.use(timeout('5s'));
app.use((req, res, next) => {
if (!req.timedout) next();
});
// 4. CAPTCHA for sensitive operations
// Google reCAPTCHA v3
<script src="https://www.google.com/recaptcha/api.js"></script>
<div class="g-recaptcha" data-sitekey="your-site-key"></div>
// Verify on backend
const axios = require('axios');
async function verifyCaptcha(token) {
const response = await axios.post(
'https://www.google.com/recaptcha/api/siteverify',
{
secret: process.env.RECAPTCHA_SECRET,
response: token
}
);
return response.data.success;
}
// 5. Input Validation
const Joi = require('joi');
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(100).required()
});
app.post('/register', (req, res) => {
const { error } = schema.validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
// Process registration
});
// 6. API Gateway / CDN Protection
// Use Cloudflare, AWS WAF, or similar
// They provide DDoS protection at edge
// 7. Queue System for Heavy Operations
const Queue = require('bull');
const emailQueue = new Queue('email', process.env.REDIS_URL);
app.post('/send-email', async (req, res) => {
await emailQueue.add({ email: req.body.email });
res.send('Email queued');
});
emailQueue.process(async (job) => {
await sendEmail(job.data.email);
});Infrastructure Level:
# nginx configuration
# Limit connections per IP
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10;
# Limit requests
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req zone=one burst=20;
# Timeout settings
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;Best Practices:
- ✅ Implement rate limiting on all endpoints
- ✅ Use CDN (Cloudflare, CloudFront) for DDoS protection
- ✅ Set request size limits
- ✅ Use timeouts for all operations
- ✅ Monitor traffic patterns
- ✅ Use auto-scaling
- ✅ Implement health checks
- ✅ Use queue systems for heavy tasks
// Using Helmet.js (Express)
const helmet = require('helmet');
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"]
}
},
// X-Frame-Options (Clickjacking)
frameguard: { action: 'deny' },
// X-Content-Type-Options (MIME sniffing)
contentTypeOptions: true, // nosniff
// Strict-Transport-Security (HTTPS)
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// X-XSS-Protection (legacy)
xssFilter: true,
// Referrer-Policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Permissions-Policy
permissionsPolicy: {
features: {
geolocation: ["'self'"],
camera: ["'none'"],
microphone: ["'none'"]
}
}
}));
// Manual headers
app.use((req, res, next) => {
// HSTS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Clickjacking protection
res.setHeader('X-Frame-Options', 'DENY');
// XSS Protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Referrer Policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions Policy
res.setHeader('Permissions-Policy', 'geolocation=(), camera=(), microphone=()');
next();
});// 1. Secure Password Storage
const bcrypt = require('bcrypt');
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Verify password
const isValid = await bcrypt.compare(password, hashedPassword);
// 2. Secure Session Management
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
maxAge: 1000 * 60 * 60 * 24, // 24 hours
sameSite: 'strict' // CSRF protection
}
}));
// 3. JWT Best Practices
const jwt = require('jsonwebtoken');
// Generate token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // Short-lived
);
// Refresh token (long-lived, stored securely)
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
// 4. Two-Factor Authentication (2FA)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate secret
const secret = speakeasy.generateSecret({ name: 'MyApp' });
// Generate QR code
QRCode.toDataURL(secret.otpauth_url, (err, dataUrl) => {
// Show to user
});
// Verify token
const verified = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userToken
});
// 5. OAuth 2.0 / Social Login
// Use passport.js or similar
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
// Find or create user
}));// Force HTTPS redirect
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});
// Or use helmet
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
// Set up HTTPS server
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem')
};
https.createServer(options, app).listen(443);// 1. Server-side validation (CRITICAL)
const validator = require('validator');
const { body, validationResult } = require('express-validator');
app.post('/user',
// Validation middleware
body('email').isEmail().normalizeEmail(),
body('age').isInt({ min: 0, max: 120 }),
body('website').optional().isURL(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process validated data
}
);
// 2. Sanitization
const sanitizeHtml = require('sanitize-html');
const clean = sanitizeHtml(dirty, {
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
allowedAttributes: {
'a': ['href']
}
});
// 3. Client-side validation (UX only, not security)
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
// React form validation
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().min(0).max(120)
});
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema)
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</form>
);
}✅ XSS Prevention:
- Escape/sanitize user input
- Use CSP headers
- Avoid
innerHTML, usetextContent - Use DOMPurify for rich text
✅ CSRF Prevention:
- Use CSRF tokens
- SameSite cookies
- Validate custom headers
✅ Clickjacking Prevention:
- X-Frame-Options: DENY
- CSP frame-ancestors
✅ Injection Prevention:
- Parameterized queries
- Input validation
- Use ORMs
✅ DoS/DDoS Prevention:
- Rate limiting
- Request size limits
- CDN/WAF protection
- CAPTCHA for sensitive ops
✅ Authentication:
- Hash passwords (bcrypt)
- Use HTTPS only
- HttpOnly, Secure cookies
- Implement 2FA
- Short-lived JWT tokens
✅ Headers:
- Strict-Transport-Security
- Content-Security-Policy
- X-Content-Type-Options
- X-Frame-Options
- Referrer-Policy
Q: What's the difference between XSS and CSRF?
- XSS: Attacker injects malicious script into your site
- CSRF: Attacker tricks user into making unwanted request to your site
Q: How do you prevent XSS in React?
- React auto-escapes by default
- Avoid
dangerouslySetInnerHTML - Sanitize with DOMPurify if HTML needed
- Use CSP headers
Q: Explain CORS and why it's important
- Restricts which origins can access your API
- Prevents malicious sites from stealing data
app.use(cors({
origin: 'https://trusted-site.com',
credentials: true
}));Q: What are httpOnly and secure flags for cookies?
- httpOnly: Prevents JavaScript access (XSS protection)
- secure: Only sent over HTTPS
- sameSite: CSRF protection
Q: How do you secure JWT tokens?
- Short expiration (15 min)
- Store in httpOnly cookies, not localStorage
- Use refresh tokens
- Validate signature
- Use strong secret key
// Real User Monitoring (RUM)
import { onCLS, onFID, onLCP } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
gtag('event', name, {
value: Math.round(name === 'CLS' ? value * 1000 : value),
event_category: 'Web Vitals',
event_label: id,
non_interaction: true
});
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
// Performance Observer API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration);
}
});
observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] });# Lighthouse CI
npm install -g @lhci/cli
lhci autorun
# Bundle size tracking
npm install -D bundlesize
# package.json
"bundlesize": [
{
"path": "./dist/bundle.js",
"maxSize": "200 kB"
}
]
# Performance budgets with Webpack
module.exports = {
performance: {
maxAssetSize: 244000,
maxEntrypointSize: 244000,
hints: 'error'
}
};Development:
- Chrome DevTools (Performance, Network, Lighthouse)
- React DevTools Profiler
- Webpack Bundle Analyzer
- Source Map Explorer
Monitoring:
- Google Analytics (Web Vitals)
- Sentry (Error tracking + Performance)
- New Relic / Datadog
- Vercel Analytics / Cloudflare Analytics
Testing:
- Lighthouse
- WebPageTest
- GTmetrix
- PageSpeed Insights
✅ Loading Performance:
- Code splitting by route
- Lazy load below-fold content
- Optimize images (WebP, responsive, lazy load)
- Minimize JavaScript bundle
- Use CDN
✅ Rendering Performance:
- Avoid layout thrashing
- Use transform/opacity for animations
- Virtualize long lists
- Debounce/throttle expensive operations
✅ Caching Strategy:
- Aggressive caching for static assets with hashes
- Stale-while-revalidate for dynamic content
- Service Worker for offline support
- CDN edge caching
✅ React Optimization:
- memo() for expensive components
- useMemo() for expensive calculations
- useCallback() for stable references
- Code split routes and heavy components
- Virtual scrolling for lists
✅ Metrics to Know:
- LCP < 2.5s
- INP < 200ms
- CLS < 0.1
- TTFB < 800ms
- FCP < 1.8s
Q: How do you optimize a slow React app?
- Profile with React DevTools
- Memoize expensive renders
- Code split large components
- Optimize re-renders (memo, Context splitting)
- Virtualize long lists
- Lazy load images/components
Q: Explain the Critical Rendering Path HTML → DOM, CSS → CSSOM, DOM + CSSOM → Render Tree → Layout → Paint → Composite
Q: How to reduce JavaScript bundle size?
- Code splitting (route & component level)
- Tree-shaking (ES modules)
- Dynamic imports
- Remove unused dependencies
- Minification & compression
Q: What's the difference between throttle and debounce?
- Throttle: Executes at most once per interval (scroll handlers)
- Debounce: Executes after quiet period (search input)
Q: How to prevent layout shifts (CLS)?
- Set explicit dimensions on images/videos
- Reserve space for ads/embeds
- Use font-display: swap
- Avoid inserting content above viewport
- Use transform for animations
Before deploying:
- [ ] Run Lighthouse audit (score > 90)
- [ ] Enable compression (gzip/brotli)
- [ ] Set proper cache headers
- [ ] Minimize main thread work
- [ ] Enable CDN
- [ ] Optimize images
- [ ] Code split routes
- [ ] Remove console.logs
- [ ] Set up error tracking
- [ ] Monitor Core Web Vitals
- [ ] Test on slow 3G network
- [ ] Test on low-end devices