Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/4e007834e697a2808af17652f2ae3b86 to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/4e007834e697a2808af17652f2ae3b86 to your computer and use it in GitHub Desktop.
Web Performance & Optimization Interview Guide

Web Performance & Optimization Interview Guide

Table of Contents

  1. Core Web Vitals
  2. Critical Rendering Path (CRP)
  3. Performance Metrics
  4. Caching Strategies
  5. Asset Optimization
  6. Bundle Optimization
  7. React-Specific Optimizations
  8. Network Optimization
  9. Runtime Performance
  10. Web Security
  11. Monitoring & Tools

Core Web Vitals

What: Google's key metrics for measuring user experience on the web.

The 3 Core Metrics

1. Largest Contentful Paint (LCP)

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

2. First Input Delay (FID) / Interaction to Next Paint (INP)

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

3. Cumulative Layout Shift (CLS)

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

Other Important Vitals

Time to First Byte (TTFB)

Target: < 800ms

  • Server response time
  • CDN usage
  • Database optimization

First Contentful Paint (FCP)

Target: < 1.8s

  • When first DOM content renders
  • Critical CSS inline
  • Remove render-blocking resources

Critical Rendering Path (CRP)

What: The sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on screen.

The CRP Steps

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)

Optimization Strategies

1. Minimize Critical Resources

<!-- 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>

2. Minimize Critical Bytes

// Minify and compress
// Before: 100KB uncompressed
// After: 25KB minified + gzipped

// Use tree-shaking
import { specificFunction } from 'library'; // Good
// import * as library from 'library'; // Bad

3. Reduce CRP Length

<!-- 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>

Resource Loading Attributes

<!-- 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 Metrics

Key Performance Metrics

// 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);

Web Vitals Measurement

// 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);

Caching Strategies

1. Browser Caching (HTTP Headers)

# 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

2. Service Worker Caching

// 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;
}

3. Memory Caching (Application Level)

// 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
  });
}

4. CDN Caching

// 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;
}

Asset Optimization

1. Image Optimization

// 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

2. Font Optimization

/* 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>

3. CSS Optimization

// 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

4. JavaScript Optimization

// 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
}

Bundle Optimization

1. Code Splitting

// 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();
});

2. Webpack Configuration

// 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'
  }
};

3. Vite Configuration

// 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
      }
    }
  }
};

4. Analyzing Bundle Size

# 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()]
};

React-Specific Optimizations

1. Component Optimization

// 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} />;
}

2. Virtualization (Large Lists)

// 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} />}
/>

3. Concurrent Features (React 18+)

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} />;
}

4. State Management Optimization

// 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);
}

5. Suspense & Streaming SSR

// 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

Network Optimization

1. HTTP/2 & HTTP/3

  • 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;

2. Resource Hints

<!-- 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">

3. Compression

// 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;

4. API Optimization

// 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')
]);

Runtime Performance

1. Avoid Layout Thrashing

// 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);
}

2. Efficient Event Handling

// 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);
}, []);

3. Intersection Observer (Lazy Loading)

// 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'));

4. Web Workers

// 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;
}

Web Security

1. Cross-Site Scripting (XSS)

What: Injection of malicious scripts into trusted websites.

Types:

  1. Stored XSS: Malicious script stored in database
  2. Reflected XSS: Script in URL/input reflected back
  3. DOM-based XSS: Client-side script manipulation

Prevention

// ❌ 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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  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 textContent instead of innerHTML
  • ✅ Validate input on both client and server
  • ✅ Use HttpOnly cookies for sensitive data

2. Cross-Site Request Forgery (CSRF)

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 -->

Prevention

// 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
    }
  });
}

3. Content Security Policy (CSP)

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

CSP Directives

// 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-report

4. Clickjacking

What: 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 -->

Prevention

// 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'"
        }
      ]
    }];
  }
};

5. SQL Injection

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

6. Denial of Service (DoS) & DDoS

What:

  • DoS: Single source overwhelming server
  • DDoS: Multiple sources (distributed) overwhelming server

Prevention Strategies

// 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

7. Additional Security Headers

// 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();
});

8. Authentication & Session Security

// 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
}));

9. HTTPS & TLS

// 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);

10. Input Validation & Sanitization

// 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>
  );
}

Security Checklist

XSS Prevention:

  • Escape/sanitize user input
  • Use CSP headers
  • Avoid innerHTML, use textContent
  • 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

Common Security Interview Questions

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

1. Performance Monitoring

// 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'] });

2. Development Tools

# 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'
  }
};

3. Key Tools

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

Quick Interview Tips

Performance Checklist

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

Common Interview Questions

Q: How do you optimize a slow React app?

  1. Profile with React DevTools
  2. Memoize expensive renders
  3. Code split large components
  4. Optimize re-renders (memo, Context splitting)
  5. Virtualize long lists
  6. 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

Production Checklist

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment