{"id":"best-practices","name":"best-practices","summary":"セキュリティ、互換性、コード品質に関して最新のウェブ開発のベストプラクティスを適用しましょう。「ベストプラクティスの適用」「セキュリティ監査」「コードのモダナイズ」「コード品質レビュー」「脆弱性チェック」などを求められた際に使ってください。","body":"# Best practices\n\nModern web development standards based on Lighthouse best practices audits. Covers security, browser compatibility, and code quality patterns.\n\n## Security\n\n### HTTPS everywhere\n\n**Enforce HTTPS:**\n```html\n<!-- ❌ Mixed content -->\n<img src=\"http://example.com/image.jpg\">\n<script src=\"http://cdn.example.com/script.js\"></script>\n\n<!-- ✅ HTTPS only -->\n<img src=\"https://example.com/image.jpg\">\n<script src=\"https://cdn.example.com/script.js\"></script>\n```\n\nAvoid protocol-relative URLs (`//example.com/...`) — they're an HTTP-era pattern with no benefit on HTTPS-only sites and hide the actual scheme from reviewers.\n\n**HSTS Header:**\n```\nStrict-Transport-Security: max-age=31536000; includeSubDomains; preload\n```\n\n### Content Security Policy (CSP)\n\n```html\n<!-- Basic CSP via meta tag -->\n<meta http-equiv=\"Content-Security-Policy\" \n      content=\"default-src 'self'; \n               script-src 'self' https://trusted-cdn.com; \n               style-src 'self' 'unsafe-inline';\n               img-src 'self' data: https:;\n               connect-src 'self' https://api.example.com;\">\n\n<!-- Better: HTTP header -->\n```\n\n**CSP Header (recommended):**\n```\nContent-Security-Policy: \n  default-src 'self';\n  script-src 'self' 'nonce-abc123' https://trusted.com;\n  style-src 'self' 'nonce-abc123';\n  img-src 'self' data: https:;\n  connect-src 'self' https://api.example.com;\n  frame-ancestors 'self';\n  base-uri 'self';\n  form-action 'self';\n```\n\n**Using nonces for inline scripts:**\n```html\n<script nonce=\"abc123\">\n  // This inline script is allowed\n</script>\n```\n\n### Trusted Types (modern DOM-XSS defense)\n\nA strict CSP blocks loading untrusted *script files*, but it doesn't stop a string from reaching `innerHTML`, `eval`, or other DOM-XSS sinks. Trusted Types — Baseline across all major browsers since early 2026 — closes that hole by making sinks reject raw strings and accept only typed objects produced by a named policy.\n\n```\nContent-Security-Policy: require-trusted-types-for 'script'; trusted-types default;\n```\n\n```javascript\n// One central policy that does the sanitization\nconst escape = trustedTypes.createPolicy('default', {\n  createHTML: (s) => DOMPurify.sanitize(s, { RETURN_TRUSTED_TYPE: true })\n});\n\n// ❌ This now throws TypeError under enforcement\nelement.innerHTML = userInput;\n\n// ✅ Goes through the policy\nelement.innerHTML = escape.createHTML(userInput);\n```\n\nRoll out with `Content-Security-Policy-Report-Only` first to find every sink usage in your app, then flip to enforcement. Angular has built-in Trusted Types support; React 19+ produces TrustedHTML when Trusted Types are enforced; for everything else, [DOMPurify](https://github.com/cure53/DOMPurify) is the de-facto sanitizer.\n\n### Subresource Integrity (SRI) for third-party scripts\n\nPin every `<script>` and `<link rel=\"stylesheet\">` you load from a CDN you don't control. If the CDN is compromised — as happened to polyfill.io in 2024 — the browser refuses to execute a file whose hash doesn't match.\n\n```html\n<script src=\"https://cdn.example.com/lib@1.2.3/dist/lib.js\"\n        integrity=\"sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC\"\n        crossorigin=\"anonymous\"></script>\n```\n\n`integrity` accepts space-separated hashes; include the next version's hash before rotating to avoid downtime. Generate with `openssl dgst -sha384 -binary file.js | openssl base64 -A`. SRI requires `crossorigin` and an `Access-Control-Allow-Origin` response header from the CDN.\n\n### Security headers\n\n```\n# Prevent clickjacking — prefer CSP `frame-ancestors` (above); X-Frame-Options\n# is the legacy fallback for older browsers.\nX-Frame-Options: DENY\n\n# Prevent MIME type sniffing\nX-Content-Type-Options: nosniff\n\n# Do NOT send X-XSS-Protection. The legacy browser XSS auditor was deprecated\n# and removed (Chrome 78, Edge 17), and in some cases it introduced its own\n# vulnerabilities. Use a strict CSP + Trusted Types (below) instead.\n\n# Control referrer information\nReferrer-Policy: strict-origin-when-cross-origin\n\n# Permissions policy (formerly Feature-Policy)\nPermissions-Policy: geolocation=(), microphone=(), camera=()\n```\n\n### No vulnerable libraries\n\n```bash\n# Check for vulnerabilities\nnpm audit\nyarn audit\n\n# Auto-fix when possible\nnpm audit fix\n\n# Check specific package\nnpm ls lodash\n```\n\n**Keep dependencies updated:**\n```json\n// package.json\n{\n  \"scripts\": {\n    \"audit\": \"npm audit --audit-level=moderate\",\n    \"update\": \"npm update && npm audit fix\"\n  }\n}\n```\n\n**Known vulnerable patterns to avoid:**\n```javascript\n// ❌ Recursive merges of untrusted input can pollute Object.prototype\n//    via __proto__, constructor, or prototype keys.\n_.merge(target, userInput);          // lodash <4.17.20\n$.extend(true, {}, target, userInput); // jQuery deep extend\nObject.assign(target, ...userInputs); // safe by itself (shallow), but unsafe\n                                      // when target IS Object.prototype-derived\n                                      // and userInput contains __proto__\n\n// ✅ For untrusted bags, use a null-prototype object so __proto__ is just a key\nconst safe = Object.create(null);\nObject.assign(safe, userInput); // shallow, no recursion → safe by construction\n\n// ✅ For deep copies, structuredClone drops __proto__ and functions\nconst deepSafe = structuredClone(userInput);\n\n// ✅ For deep merges, use a library that explicitly blocks dangerous keys\n//    (e.g. lodash ≥4.17.21 _.mergeWith with a customizer, or deepmerge-ts).\n```\n\n### Input sanitization\n\n```javascript\n// ❌ XSS vulnerable\nelement.innerHTML = userInput;\ndocument.write(userInput);\n\n// ✅ Safe text content\nelement.textContent = userInput;\n\n// ✅ If HTML needed, sanitize\nimport DOMPurify from 'dompurify';\nelement.innerHTML = DOMPurify.sanitize(userInput);\n```\n\n### Secure cookies\n\n```javascript\n// ❌ Insecure cookie\ndocument.cookie = \"session=abc123\";\n\n// ✅ Secure cookie (server-side)\nSet-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/\n```\n\n---\n\n## Browser compatibility\n\n### Doctype declaration\n\n```html\n<!-- ❌ Missing or invalid doctype -->\n<HTML>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n\n<!-- ✅ HTML5 doctype -->\n<!DOCTYPE html>\n<html lang=\"en\">\n```\n\n### Character encoding\n\n```html\n<!-- ❌ Missing or late charset -->\n<html>\n<head>\n  <title>Page</title>\n  <meta charset=\"UTF-8\">\n</head>\n\n<!-- ✅ Charset as first element in head -->\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Page</title>\n</head>\n```\n\n### Viewport meta tag\n\n```html\n<!-- ❌ Missing viewport -->\n<head>\n  <title>Page</title>\n</head>\n\n<!-- ✅ Responsive viewport -->\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Page</title>\n</head>\n```\n\n### Feature detection\n\n```javascript\n// ❌ Browser detection (brittle)\nif (navigator.userAgent.includes('Chrome')) {\n  // Chrome-specific code\n}\n\n// ✅ Feature detection\nif ('IntersectionObserver' in window) {\n  // Use IntersectionObserver\n} else {\n  // Fallback\n}\n\n// ✅ Using @supports in CSS\n@supports (display: grid) {\n  .container {\n    display: grid;\n  }\n}\n\n@supports not (display: grid) {\n  .container {\n    display: flex;\n  }\n}\n```\n\n### Polyfills (when needed)\n\nPrefer **bundling polyfills at build time** (Babel/SWC + `core-js`, or `@vitejs/plugin-legacy`) targeted by your supported-browsers list. This eliminates the runtime check entirely and avoids shipping polyfill bytes to modern browsers.\n\nIf you must load a polyfill at runtime, append a script element — never use `document.write` (it blocks the parser and is broken in async/deferred contexts):\n\n```html\n<script>\n  if (!('fetch' in window)) {\n    const s = document.createElement('script');\n    s.src = '/polyfills/fetch.js';\n    s.defer = true;\n    document.head.appendChild(s);\n  }\n</script>\n```\n\n**Never load polyfills from a third-party CDN you don't control.** The `polyfill.io` service was [compromised in mid-2024](https://sansec.io/research/polyfill-supply-chain-attack) in a supply-chain attack and used to serve malware to ~100k sites. Self-host, or use a vetted mirror (e.g. [Cloudflare's `cdnjs` polyfill build](https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/)) — and pin the version with [Subresource Integrity](#subresource-integrity-sri-for-third-party-scripts).\n\n---\n\n## Deprecated APIs\n\n### Avoid these\n\n```javascript\n// ❌ document.write (blocks parsing)\ndocument.write('<script src=\"...\"></script>');\n\n// ✅ Dynamic script loading\nconst script = document.createElement('script');\nscript.src = '...';\ndocument.head.appendChild(script);\n\n// ❌ Synchronous XHR (blocks main thread)\nconst xhr = new XMLHttpRequest();\nxhr.open('GET', url, false); // false = synchronous\n\n// ✅ Async fetch\nconst response = await fetch(url);\n\n// ❌ Application Cache (deprecated)\n<html manifest=\"cache.manifest\">\n\n// ✅ Service Workers\nif ('serviceWorker' in navigator) {\n  navigator.serviceWorker.register('/sw.js');\n}\n```\n\n### Event listener passive\n\n```javascript\n// ❌ Non-passive touch/wheel (may block scrolling)\nelement.addEventListener('touchstart', handler);\nelement.addEventListener('wheel', handler);\n\n// ✅ Passive listeners (allows smooth scrolling)\nelement.addEventListener('touchstart', handler, { passive: true });\nelement.addEventListener('wheel', handler, { passive: true });\n\n// ✅ If you need preventDefault, be explicit\nelement.addEventListener('touchstart', handler, { passive: false });\n```\n\n---\n\n## Console & errors\n\n### No console errors\n\n```javascript\n// ❌ Errors in production\nconsole.log('Debug info'); // Remove in production\nthrow new Error('Unhandled'); // Catch all errors\n\n// ✅ Proper error handling\ntry {\n  riskyOperation();\n} catch (error) {\n  // Log to error tracking service\n  errorTracker.captureException(error);\n  // Show user-friendly message\n  showErrorMessage('Something went wrong. Please try again.');\n}\n```\n\n### Error boundaries (React)\n\n```jsx\nclass ErrorBoundary extends React.Component {\n  state = { hasError: false };\n  \n  static getDerivedStateFromError(error) {\n    return { hasError: true };\n  }\n  \n  componentDidCatch(error, info) {\n    errorTracker.captureException(error, { extra: info });\n  }\n  \n  render() {\n    if (this.state.hasError) {\n      return <FallbackUI />;\n    }\n    return this.props.children;\n  }\n}\n\n// Usage\n<ErrorBoundary>\n  <App />\n</ErrorBoundary>\n```\n\n### Global error handler\n\n```javascript\n// Catch unhandled errors\nwindow.addEventListener('error', (event) => {\n  errorTracker.captureException(event.error);\n});\n\n// Catch unhandled promise rejections\nwindow.addEventListener('unhandledrejection', (event) => {\n  errorTracker.captureException(event.reason);\n});\n```\n\n---\n\n## Source maps\n\n### Production configuration\n\n```javascript\n// ❌ Source maps exposed in production\n// webpack.config.js\nmodule.exports = {\n  devtool: 'source-map', // Exposes source code\n};\n\n// ✅ Hidden source maps (uploaded to error tracker)\nmodule.exports = {\n  devtool: 'hidden-source-map',\n};\n\n// ✅ Or no source maps in production\nmodule.exports = {\n  devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',\n};\n```\n\n**Strip `sourcesContent` from production maps** when uploading to your error tracker. By default, bundlers embed the full original source inside the `.map` file — anyone who obtains the map (including via a misconfigured upload step) gets your unminified code. Configure your bundler to omit `sourcesContent`, or use a Sentry/Bugsnag CLI flag that does so when uploading.\n\nFor Vite, prefer `sourcemap: 'hidden'` over `'true'` so the `//# sourceMappingURL=` comment isn't emitted into the bundle.\n\n---\n\n## Performance best practices\n\n### Avoid blocking patterns\n\n```javascript\n// ❌ Blocking script\n<script src=\"heavy-library.js\"></script>\n\n// ✅ Deferred script\n<script defer src=\"heavy-library.js\"></script>\n\n// ❌ Blocking CSS import\n@import url('other-styles.css');\n\n// ✅ Link tags (parallel loading)\n<link rel=\"stylesheet\" href=\"styles.css\">\n<link rel=\"stylesheet\" href=\"other-styles.css\">\n```\n\n### Efficient event handlers\n\n```javascript\n// ❌ Handler on every element\nitems.forEach(item => {\n  item.addEventListener('click', handleClick);\n});\n\n// ✅ Event delegation\ncontainer.addEventListener('click', (e) => {\n  if (e.target.matches('.item')) {\n    handleClick(e);\n  }\n});\n```\n\n### Memory management\n\n```javascript\n// ❌ Memory leak (never removed)\nconst handler = () => { /* ... */ };\nwindow.addEventListener('resize', handler);\n\n// ✅ Cleanup when done\nconst handler = () => { /* ... */ };\nwindow.addEventListener('resize', handler);\n\n// Later, when component unmounts:\nwindow.removeEventListener('resize', handler);\n\n// ✅ Using AbortController\nconst controller = new AbortController();\nwindow.addEventListener('resize', handler, { signal: controller.signal });\n\n// Cleanup:\ncontroller.abort();\n```\n\n---\n\n## Code quality\n\n### Valid HTML\n\n```html\n<!-- ❌ Invalid HTML -->\n<div id=\"header\">\n<div id=\"header\"> <!-- Duplicate ID -->\n\n<ul>\n  <div>Item</div> <!-- Invalid child -->\n</ul>\n\n<a href=\"/\"><button>Click</button></a> <!-- Invalid nesting -->\n\n<!-- ✅ Valid HTML -->\n<header id=\"site-header\">\n</header>\n\n<ul>\n  <li>Item</li>\n</ul>\n\n<a href=\"/\" class=\"button\">Click</a>\n```\n\n### Semantic HTML\n\n```html\n<!-- ❌ Non-semantic -->\n<div class=\"header\">\n  <div class=\"nav\">\n    <div class=\"nav-item\">Home</div>\n  </div>\n</div>\n<div class=\"main\">\n  <div class=\"article\">\n    <div class=\"title\">Headline</div>\n  </div>\n</div>\n\n<!-- ✅ Semantic HTML5 -->\n<header>\n  <nav>\n    <a href=\"/\">Home</a>\n  </nav>\n</header>\n<main>\n  <article>\n    <h1>Headline</h1>\n  </article>\n</main>\n```\n\n### Image aspect ratios\n\n```html\n<!-- ❌ Distorted images -->\n<img src=\"photo.jpg\" width=\"300\" height=\"100\">\n<!-- If actual ratio is 4:3, this squishes the image -->\n\n<!-- ✅ Preserve aspect ratio -->\n<img src=\"photo.jpg\" width=\"300\" height=\"225\">\n<!-- Actual 4:3 dimensions -->\n\n<!-- ✅ CSS object-fit for flexibility -->\n<img src=\"photo.jpg\" style=\"width: 300px; height: 200px; object-fit: cover;\">\n```\n\n---\n\n## Permissions & privacy\n\n### Request permissions properly\n\n```javascript\n// ❌ Request on page load (bad UX, often denied)\nnavigator.geolocation.getCurrentPosition(success, error);\n\n// ✅ Request in context, after user action\nfindNearbyButton.addEventListener('click', async () => {\n  // Explain why you need it\n  if (await showPermissionExplanation()) {\n    navigator.geolocation.getCurrentPosition(success, error);\n  }\n});\n```\n\n### Permissions policy\n\n```html\n<!-- Restrict powerful features -->\n<meta http-equiv=\"Permissions-Policy\" \n      content=\"geolocation=(), camera=(), microphone=()\">\n\n<!-- Or allow for specific origins -->\n<meta http-equiv=\"Permissions-Policy\" \n      content=\"geolocation=(self 'https://maps.example.com')\">\n```\n\n---\n\n## Audit checklist\n\n### Security (critical)\n- [ ] HTTPS enabled, no mixed content\n- [ ] No vulnerable dependencies (`npm audit`)\n- [ ] CSP headers configured (with `frame-ancestors`, `base-uri`, `form-action`)\n- [ ] `require-trusted-types-for 'script'` enforced (or report-only during rollout)\n- [ ] Third-party `<script>`/`<link rel=\"stylesheet\">` pinned with SRI hashes\n- [ ] Security headers present (HSTS, X-Content-Type-Options, Referrer-Policy)\n- [ ] No exposed source maps (and `sourcesContent` stripped from uploaded ones)\n\n### Compatibility\n- [ ] Valid HTML5 doctype\n- [ ] Charset declared first in head\n- [ ] Viewport meta tag present\n- [ ] No deprecated APIs used\n- [ ] Passive event listeners for scroll/touch\n\n### Code quality\n- [ ] No console errors\n- [ ] Valid HTML (no duplicate IDs)\n- [ ] Semantic HTML elements used\n- [ ] Proper error handling\n- [ ] Memory cleanup in components\n\n### UX\n- [ ] No intrusive interstitials\n- [ ] Permission requests in context\n- [ ] Clear error messages\n- [ ] Appropriate image aspect ratios\n\n## Tools\n\n| Tool | Purpose |\n|------|---------|\n| `npm audit` | Dependency vulnerabilities |\n| [SecurityHeaders.com](https://securityheaders.com) | Header analysis |\n| [W3C Validator](https://validator.w3.org) | HTML validation |\n| Lighthouse | Best practices audit |\n| [Observatory](https://observatory.mozilla.org) | Security scan |\n\n## References\n\n- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [Web Quality Audit](../web-quality-audit/SKILL.md)","author":"@addyosmani","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/addyosmani/web-quality-skills/tree/main/skills/best-practices","license":"MIT","category":"review","lang":"en","tokens":4018,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["api.example.com","blog.cloudflare.com","cdn.example.com","maps.example.com","observatory.mozilla.org","owasp.org","sansec.io","securityheaders.com","trusted-cdn.com","trusted.com","validator.w3.org"]}}