Best practices
Modern web development standards based on Lighthouse best practices audits. Covers security, browser compatibility, and code quality patterns.
Security
HTTPS everywhere
Enforce HTTPS:
html
1<!-- ❌ Mixed content -->
2<img src="http://example.com/image.jpg">
3<script src="http://cdn.example.com/script.js"></script>
4
5<!-- ✅ HTTPS only -->
6<img src="https://example.com/image.jpg">
7<script src="https://cdn.example.com/script.js"></script>
8
9<!-- ✅ Protocol-relative (will use page's protocol) -->
10<img src="//example.com/image.jpg">
HSTS Header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content Security Policy (CSP)
html
1<!-- Basic CSP via meta tag -->
2<meta http-equiv="Content-Security-Policy"
3 content="default-src 'self';
4 script-src 'self' https://trusted-cdn.com;
5 style-src 'self' 'unsafe-inline';
6 img-src 'self' data: https:;
7 connect-src 'self' https://api.example.com;">
8
9<!-- Better: HTTP header -->
CSP Header (recommended):
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123' https://trusted.com;
style-src 'self' 'nonce-abc123';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
Using nonces for inline scripts:
html
1<script nonce="abc123">
2 // This inline script is allowed
3</script>
# Prevent clickjacking
X-Frame-Options: DENY
# Prevent MIME type sniffing
X-Content-Type-Options: nosniff
# Enable XSS filter (legacy browsers)
X-XSS-Protection: 1; mode=block
# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin
# Permissions policy (formerly Feature-Policy)
Permissions-Policy: geolocation=(), microphone=(), camera=()
No vulnerable libraries
bash
1# Check for vulnerabilities
2npm audit
3yarn audit
4
5# Auto-fix when possible
6npm audit fix
7
8# Check specific package
9npm ls lodash
Keep dependencies updated:
json
1// package.json
2{
3 "scripts": {
4 "audit": "npm audit --audit-level=moderate",
5 "update": "npm update && npm audit fix"
6 }
7}
Known vulnerable patterns to avoid:
javascript
1// ❌ Prototype pollution vulnerable patterns
2Object.assign(target, userInput);
3_.merge(target, userInput);
4
5// ✅ Safer alternatives
6const safeData = JSON.parse(JSON.stringify(userInput));
javascript
1// ❌ XSS vulnerable
2element.innerHTML = userInput;
3document.write(userInput);
4
5// ✅ Safe text content
6element.textContent = userInput;
7
8// ✅ If HTML needed, sanitize
9import DOMPurify from 'dompurify';
10element.innerHTML = DOMPurify.sanitize(userInput);
Secure cookies
javascript
1// ❌ Insecure cookie
2document.cookie = "session=abc123";
3
4// ✅ Secure cookie (server-side)
5Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/
Browser compatibility
Doctype declaration
html
1<!-- ❌ Missing or invalid doctype -->
2<HTML>
3<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
4
5<!-- ✅ HTML5 doctype -->
6<!DOCTYPE html>
7<html lang="en">
Character encoding
html
1<!-- ❌ Missing or late charset -->
2<html>
3<head>
4 <title>Page</title>
5 <meta charset="UTF-8">
6</head>
7
8<!-- ✅ Charset as first element in head -->
9<html>
10<head>
11 <meta charset="UTF-8">
12 <title>Page</title>
13</head>
html
1<!-- ❌ Missing viewport -->
2<head>
3 <title>Page</title>
4</head>
5
6<!-- ✅ Responsive viewport -->
7<head>
8 <meta charset="UTF-8">
9 <meta name="viewport" content="width=device-width, initial-scale=1">
10 <title>Page</title>
11</head>
Feature detection
javascript
1// ❌ Browser detection (brittle)
2if (navigator.userAgent.includes('Chrome')) {
3 // Chrome-specific code
4}
5
6// ✅ Feature detection
7if ('IntersectionObserver' in window) {
8 // Use IntersectionObserver
9} else {
10 // Fallback
11}
12
13// ✅ Using @supports in CSS
14@supports (display: grid) {
15 .container {
16 display: grid;
17 }
18}
19
20@supports not (display: grid) {
21 .container {
22 display: flex;
23 }
24}
Polyfills (when needed)
html
1<!-- Load polyfills conditionally -->
2<script>
3 if (!('fetch' in window)) {
4 document.write('<script src="/polyfills/fetch.js"><\/script>');
5 }
6</script>
7
8<!-- Or use polyfill.io -->
9<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,IntersectionObserver"></script>
Deprecated APIs
Avoid these
javascript
1// ❌ document.write (blocks parsing)
2document.write('<script src="..."></script>');
3
4// ✅ Dynamic script loading
5const script = document.createElement('script');
6script.src = '...';
7document.head.appendChild(script);
8
9// ❌ Synchronous XHR (blocks main thread)
10const xhr = new XMLHttpRequest();
11xhr.open('GET', url, false); // false = synchronous
12
13// ✅ Async fetch
14const response = await fetch(url);
15
16// ❌ Application Cache (deprecated)
17<html manifest="cache.manifest">
18
19// ✅ Service Workers
20if ('serviceWorker' in navigator) {
21 navigator.serviceWorker.register('/sw.js');
22}
Event listener passive
javascript
1// ❌ Non-passive touch/wheel (may block scrolling)
2element.addEventListener('touchstart', handler);
3element.addEventListener('wheel', handler);
4
5// ✅ Passive listeners (allows smooth scrolling)
6element.addEventListener('touchstart', handler, { passive: true });
7element.addEventListener('wheel', handler, { passive: true });
8
9// ✅ If you need preventDefault, be explicit
10element.addEventListener('touchstart', handler, { passive: false });
Console & errors
No console errors
javascript
1// ❌ Errors in production
2console.log('Debug info'); // Remove in production
3throw new Error('Unhandled'); // Catch all errors
4
5// ✅ Proper error handling
6try {
7 riskyOperation();
8} catch (error) {
9 // Log to error tracking service
10 errorTracker.captureException(error);
11 // Show user-friendly message
12 showErrorMessage('Something went wrong. Please try again.');
13}
Error boundaries (React)
jsx
1class ErrorBoundary extends React.Component {
2 state = { hasError: false };
3
4 static getDerivedStateFromError(error) {
5 return { hasError: true };
6 }
7
8 componentDidCatch(error, info) {
9 errorTracker.captureException(error, { extra: info });
10 }
11
12 render() {
13 if (this.state.hasError) {
14 return <FallbackUI />;
15 }
16 return this.props.children;
17 }
18}
19
20// Usage
21<ErrorBoundary>
22 <App />
23</ErrorBoundary>
Global error handler
javascript
1// Catch unhandled errors
2window.addEventListener('error', (event) => {
3 errorTracker.captureException(event.error);
4});
5
6// Catch unhandled promise rejections
7window.addEventListener('unhandledrejection', (event) => {
8 errorTracker.captureException(event.reason);
9});
Source maps
Production configuration
javascript
1// ❌ Source maps exposed in production
2// webpack.config.js
3module.exports = {
4 devtool: 'source-map', // Exposes source code
5};
6
7// ✅ Hidden source maps (uploaded to error tracker)
8module.exports = {
9 devtool: 'hidden-source-map',
10};
11
12// ✅ Or no source maps in production
13module.exports = {
14 devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
15};
Avoid blocking patterns
javascript
1// ❌ Blocking script
2<script src="heavy-library.js"></script>
3
4// ✅ Deferred script
5<script defer src="heavy-library.js"></script>
6
7// ❌ Blocking CSS import
8@import url('other-styles.css');
9
10// ✅ Link tags (parallel loading)
11<link rel="stylesheet" href="styles.css">
12<link rel="stylesheet" href="other-styles.css">
Efficient event handlers
javascript
1// ❌ Handler on every element
2items.forEach(item => {
3 item.addEventListener('click', handleClick);
4});
5
6// ✅ Event delegation
7container.addEventListener('click', (e) => {
8 if (e.target.matches('.item')) {
9 handleClick(e);
10 }
11});
Memory management
javascript
1// ❌ Memory leak (never removed)
2const handler = () => { /* ... */ };
3window.addEventListener('resize', handler);
4
5// ✅ Cleanup when done
6const handler = () => { /* ... */ };
7window.addEventListener('resize', handler);
8
9// Later, when component unmounts:
10window.removeEventListener('resize', handler);
11
12// ✅ Using AbortController
13const controller = new AbortController();
14window.addEventListener('resize', handler, { signal: controller.signal });
15
16// Cleanup:
17controller.abort();
Code quality
Valid HTML
html
1<!-- ❌ Invalid HTML -->
2<div id="header">
3<div id="header"> <!-- Duplicate ID -->
4
5<ul>
6 <div>Item</div> <!-- Invalid child -->
7</ul>
8
9<a href="/"><button>Click</button></a> <!-- Invalid nesting -->
10
11<!-- ✅ Valid HTML -->
12<header id="site-header">
13</header>
14
15<ul>
16 <li>Item</li>
17</ul>
18
19<a href="/" class="button">Click</a>
Semantic HTML
html
1<!-- ❌ Non-semantic -->
2<div class="header">
3 <div class="nav">
4 <div class="nav-item">Home</div>
5 </div>
6</div>
7<div class="main">
8 <div class="article">
9 <div class="title">Headline</div>
10 </div>
11</div>
12
13<!-- ✅ Semantic HTML5 -->
14<header>
15 <nav>
16 <a href="/">Home</a>
17 </nav>
18</header>
19<main>
20 <article>
21 <h1>Headline</h1>
22 </article>
23</main>
Image aspect ratios
html
1<!-- ❌ Distorted images -->
2<img src="photo.jpg" width="300" height="100">
3<!-- If actual ratio is 4:3, this squishes the image -->
4
5<!-- ✅ Preserve aspect ratio -->
6<img src="photo.jpg" width="300" height="225">
7<!-- Actual 4:3 dimensions -->
8
9<!-- ✅ CSS object-fit for flexibility -->
10<img src="photo.jpg" style="width: 300px; height: 200px; object-fit: cover;">
Permissions & privacy
Request permissions properly
javascript
1// ❌ Request on page load (bad UX, often denied)
2navigator.geolocation.getCurrentPosition(success, error);
3
4// ✅ Request in context, after user action
5findNearbyButton.addEventListener('click', async () => {
6 // Explain why you need it
7 if (await showPermissionExplanation()) {
8 navigator.geolocation.getCurrentPosition(success, error);
9 }
10});
Permissions policy
html
1<!-- Restrict powerful features -->
2<meta http-equiv="Permissions-Policy"
3 content="geolocation=(), camera=(), microphone=()">
4
5<!-- Or allow for specific origins -->
6<meta http-equiv="Permissions-Policy"
7 content="geolocation=(self 'https://maps.example.com')">
Audit checklist
Security (critical)
Compatibility
Code quality
UX
References