best-practices

Áp dụng các phương pháp thực hành tốt nhất trong phát triển web hiện đại về bảo mật, khả năng tương thích và chất lượng mã. Sử dụng khi được yêu cầu "áp dụng các phương pháp thực hành tốt nhất", "kiểm tra bảo mật", "hiện đại hóa mã", "đánh giá chất lượng mã" hoặc "kiểm tra lỗ hổng".

npx skills add https://github.com/addyosmani/web-quality-skills --skill best-practices

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:

<!-- ❌ Mixed content -->
<img src="http://example.com/image.jpg">
<script src="http://cdn.example.com/script.js"></script>

<!-- ✅ HTTPS only -->
<img src="https://example.com/image.jpg">
<script src="https://cdn.example.com/script.js"></script>

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

HSTS Header:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Content Security Policy (CSP)

<!-- Basic CSP via meta tag -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' https://trusted-cdn.com; 
               style-src 'self' 'unsafe-inline';
               img-src 'self' data: https:;
               connect-src 'self' https://api.example.com;">

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

<script nonce="abc123">
  // This inline script is allowed
</script>

Trusted Types (modern DOM-XSS defense)

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

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default;
// One central policy that does the sanitization
const escape = trustedTypes.createPolicy('default', {
  createHTML: (s) => DOMPurify.sanitize(s, { RETURN_TRUSTED_TYPE: true })
});

// ❌ This now throws TypeError under enforcement
element.innerHTML = userInput;

// ✅ Goes through the policy
element.innerHTML = escape.createHTML(userInput);

Roll 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 is the de-facto sanitizer.

Subresource Integrity (SRI) for third-party scripts

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

<script src="https://cdn.example.com/lib@1.2.3/dist/lib.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

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.

Security headers

# Prevent clickjacking — prefer CSP `frame-ancestors` (above); X-Frame-Options
# is the legacy fallback for older browsers.
X-Frame-Options: DENY

# Prevent MIME type sniffing
X-Content-Type-Options: nosniff

# Do NOT send X-XSS-Protection. The legacy browser XSS auditor was deprecated
# and removed (Chrome 78, Edge 17), and in some cases it introduced its own
# vulnerabilities. Use a strict CSP + Trusted Types (below) instead.

# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin

# Permissions policy (formerly Feature-Policy)
Permissions-Policy: geolocation=(), microphone=(), camera=()

No vulnerable libraries

# Check for vulnerabilities
npm audit
yarn audit

# Auto-fix when possible
npm audit fix

# Check specific package
npm ls lodash

Keep dependencies updated:

// package.json
{
  "scripts": {
    "audit": "npm audit --audit-level=moderate",
    "update": "npm update && npm audit fix"
  }
}

Known vulnerable patterns to avoid:

// ❌ Recursive merges of untrusted input can pollute Object.prototype
//    via __proto__, constructor, or prototype keys.
_.merge(target, userInput);          // lodash <4.17.20
$.extend(true, {}, target, userInput); // jQuery deep extend
Object.assign(target, ...userInputs); // safe by itself (shallow), but unsafe
                                      // when target IS Object.prototype-derived
                                      // and userInput contains __proto__

// ✅ For untrusted bags, use a null-prototype object so __proto__ is just a key
const safe = Object.create(null);
Object.assign(safe, userInput); // shallow, no recursion → safe by construction

// ✅ For deep copies, structuredClone drops __proto__ and functions
const deepSafe = structuredClone(userInput);

// ✅ For deep merges, use a library that explicitly blocks dangerous keys
//    (e.g. lodash ≥4.17.21 _.mergeWith with a customizer, or deepmerge-ts).

Input sanitization

// ❌ XSS vulnerable
element.innerHTML = userInput;
document.write(userInput);

// ✅ Safe text content
element.textContent = userInput;

// ✅ If HTML needed, sanitize
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

Secure cookies

// ❌ Insecure cookie
document.cookie = "session=abc123";

// ✅ Secure cookie (server-side)
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/

Browser compatibility

Doctype declaration

<!-- ❌ Missing or invalid doctype -->
<HTML>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">

<!-- ✅ HTML5 doctype -->
<!DOCTYPE html>
<html lang="en">

Character encoding

<!-- ❌ Missing or late charset -->
<html>
<head>
  <title>Page</title>
  <meta charset="UTF-8">
</head>

<!-- ✅ Charset as first element in head -->
<html>
<head>
  <meta charset="UTF-8">
  <title>Page</title>
</head>

Viewport meta tag

<!-- ❌ Missing viewport -->
<head>
  <title>Page</title>
</head>

<!-- ✅ Responsive viewport -->
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Page</title>
</head>

Feature detection

// ❌ Browser detection (brittle)
if (navigator.userAgent.includes('Chrome')) {
  // Chrome-specific code
}

// ✅ Feature detection
if ('IntersectionObserver' in window) {
  // Use IntersectionObserver
} else {
  // Fallback
}

// ✅ Using @supports in CSS
@supports (display: grid) {
  .container {
    display: grid;
  }
}

@supports not (display: grid) {
  .container {
    display: flex;
  }
}

Polyfills (when needed)

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

If 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):

<script>
  if (!('fetch' in window)) {
    const s = document.createElement('script');
    s.src = '/polyfills/fetch.js';
    s.defer = true;
    document.head.appendChild(s);
  }
</script>

Never load polyfills from a third-party CDN you don't control. The polyfill.io service was compromised in mid-2024 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) — and pin the version with Subresource Integrity.


Deprecated APIs

Avoid these

// ❌ document.write (blocks parsing)
document.write('<script src="..."></script>');

// ✅ Dynamic script loading
const script = document.createElement('script');
script.src = '...';
document.head.appendChild(script);

// ❌ Synchronous XHR (blocks main thread)
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // false = synchronous

// ✅ Async fetch
const response = await fetch(url);

// ❌ Application Cache (deprecated)
<html manifest="cache.manifest">

// ✅ Service Workers
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

Event listener passive

// ❌ Non-passive touch/wheel (may block scrolling)
element.addEventListener('touchstart', handler);
element.addEventListener('wheel', handler);

// ✅ Passive listeners (allows smooth scrolling)
element.addEventListener('touchstart', handler, { passive: true });
element.addEventListener('wheel', handler, { passive: true });

// ✅ If you need preventDefault, be explicit
element.addEventListener('touchstart', handler, { passive: false });

Console & errors

No console errors

// ❌ Errors in production
console.log('Debug info'); // Remove in production
throw new Error('Unhandled'); // Catch all errors

// ✅ Proper error handling
try {
  riskyOperation();
} catch (error) {
  // Log to error tracking service
  errorTracker.captureException(error);
  // Show user-friendly message
  showErrorMessage('Something went wrong. Please try again.');
}

Error boundaries (React)

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, info) {
    errorTracker.captureException(error, { extra: info });
  }
  
  render() {
    if (this.state.hasError) {
      return <FallbackUI />;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <App />
</ErrorBoundary>

Global error handler

// Catch unhandled errors
window.addEventListener('error', (event) => {
  errorTracker.captureException(event.error);
});

// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
  errorTracker.captureException(event.reason);
});

Source maps

Production configuration

// ❌ Source maps exposed in production
// webpack.config.js
module.exports = {
  devtool: 'source-map', // Exposes source code
};

// ✅ Hidden source maps (uploaded to error tracker)
module.exports = {
  devtool: 'hidden-source-map',
};

// ✅ Or no source maps in production
module.exports = {
  devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
};

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.

For Vite, prefer sourcemap: 'hidden' over 'true' so the //# sourceMappingURL= comment isn't emitted into the bundle.


Performance best practices

Avoid blocking patterns

// ❌ Blocking script
<script src="heavy-library.js"></script>

// ✅ Deferred script
<script defer src="heavy-library.js"></script>

// ❌ Blocking CSS import
@import url('other-styles.css');

// ✅ Link tags (parallel loading)
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="other-styles.css">

Efficient event handlers

// ❌ Handler on every element
items.forEach(item => {
  item.addEventListener('click', handleClick);
});

// ✅ Event delegation
container.addEventListener('click', (e) => {
  if (e.target.matches('.item')) {
    handleClick(e);
  }
});

Memory management

// ❌ Memory leak (never removed)
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);

// ✅ Cleanup when done
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);

// Later, when component unmounts:
window.removeEventListener('resize', handler);

// ✅ Using AbortController
const controller = new AbortController();
window.addEventListener('resize', handler, { signal: controller.signal });

// Cleanup:
controller.abort();

Code quality

Valid HTML

<!-- ❌ Invalid HTML -->
<div id="header">
<div id="header"> <!-- Duplicate ID -->

<ul>
  <div>Item</div> <!-- Invalid child -->
</ul>

<a href="/"><button>Click</button></a> <!-- Invalid nesting -->

<!-- ✅ Valid HTML -->
<header id="site-header">
</header>

<ul>
  <li>Item</li>
</ul>

<a href="/" class="button">Click</a>

Semantic HTML

<!-- ❌ Non-semantic -->
<div class="header">
  <div class="nav">
    <div class="nav-item">Home</div>
  </div>
</div>
<div class="main">
  <div class="article">
    <div class="title">Headline</div>
  </div>
</div>

<!-- ✅ Semantic HTML5 -->
<header>
  <nav>
    <a href="/">Home</a>
  </nav>
</header>
<main>
  <article>
    <h1>Headline</h1>
  </article>
</main>

Image aspect ratios

<!-- ❌ Distorted images -->
<img src="photo.jpg" width="300" height="100">
<!-- If actual ratio is 4:3, this squishes the image -->

<!-- ✅ Preserve aspect ratio -->
<img src="photo.jpg" width="300" height="225">
<!-- Actual 4:3 dimensions -->

<!-- ✅ CSS object-fit for flexibility -->
<img src="photo.jpg" style="width: 300px; height: 200px; object-fit: cover;">

Permissions & privacy

Request permissions properly

// ❌ Request on page load (bad UX, often denied)
navigator.geolocation.getCurrentPosition(success, error);

// ✅ Request in context, after user action
findNearbyButton.addEventListener('click', async () => {
  // Explain why you need it
  if (await showPermissionExplanation()) {
    navigator.geolocation.getCurrentPosition(success, error);
  }
});

Permissions policy

<!-- Restrict powerful features -->
<meta http-equiv="Permissions-Policy" 
      content="geolocation=(), camera=(), microphone=()">

<!-- Or allow for specific origins -->
<meta http-equiv="Permissions-Policy" 
      content="geolocation=(self 'https://maps.example.com')">

Audit checklist

Security (critical)

  • HTTPS enabled, no mixed content
  • No vulnerable dependencies (npm audit)
  • CSP headers configured (with frame-ancestors, base-uri, form-action)
  • require-trusted-types-for 'script' enforced (or report-only during rollout)
  • Third-party <script>/<link rel="stylesheet"> pinned with SRI hashes
  • Security headers present (HSTS, X-Content-Type-Options, Referrer-Policy)
  • No exposed source maps (and sourcesContent stripped from uploaded ones)

Compatibility

  • Valid HTML5 doctype
  • Charset declared first in head
  • Viewport meta tag present
  • No deprecated APIs used
  • Passive event listeners for scroll/touch

Code quality

  • No console errors
  • Valid HTML (no duplicate IDs)
  • Semantic HTML elements used
  • Proper error handling
  • Memory cleanup in components

UX

  • No intrusive interstitials
  • Permission requests in context
  • Clear error messages
  • Appropriate image aspect ratios

Tools

ToolPurpose
npm auditDependency vulnerabilities
SecurityHeaders.comHeader analysis
W3C ValidatorHTML validation
LighthouseBest practices audit
ObservatorySecurity scan

References

Thêm skills từ addyosmani

accessibility
addyosmani
Kiểm tra và cải thiện khả năng truy cập web theo hướng dẫn WCAG 2.2. Sử dụng khi được yêu cầu "cải thiện khả năng truy cập", "kiểm tra a11y", "tuân thủ WCAG", "hỗ trợ trình đọc màn hình", "điều hướng bàn phím" hoặc "làm cho có thể truy cập".
developmenttestingcode-review
web-quality-audit
addyosmani
Kiểm tra chất lượng web toàn diện bao gồm hiệu suất, khả năng tiếp cận, SEO và các phương pháp tốt nhất. Sử dụng khi được yêu cầu "kiểm tra trang web của tôi", "đánh giá chất lượng web", "chạy kiểm tra lighthouse", "kiểm tra chất lượng trang" hoặc "tối ưu hóa trang web của tôi".
developmenttestingresearch
seo
addyosmani
Tối ưu hóa khả năng hiển thị và thứ hạng trên công cụ tìm kiếm. Sử dụng khi được yêu cầu "cải thiện SEO", "tối ưu hóa cho tìm kiếm", "sửa thẻ meta", "thêm dữ liệu có cấu trúc", "tối ưu hóa sơ đồ trang web" hoặc "tối ưu hóa công cụ tìm kiếm".
marketingresearchdevelopment
performance
addyosmani
Tối ưu hiệu suất web để tải nhanh hơn và cải thiện trải nghiệm người dùng. Sử dụng khi được yêu cầu "tăng tốc trang web của tôi", "tối ưu hiệu suất", "giảm thời gian tải", "sửa tải chậm", "cải thiện tốc độ trang" hoặc "kiểm tra hiệu suất".
developmenttesting
code-review-and-quality
addyosmani
Thực hiện đánh giá mã nguồn đa chiều. Sử dụng trước khi hợp nhất bất kỳ thay đổi nào. Sử dụng khi xem xét mã do chính bạn, một tác nhân khác hoặc con người viết. Sử dụng khi bạn cần đánh giá chất lượng mã trên nhiều khía cạnh trước khi nó được đưa vào nhánh chính.
developmentcode-review
frontend-ui-engineering
addyosmani
Xây dựng giao diện người dùng chất lượng sản xuất, có khả năng truy cập và phản hồi. Sử dụng khi xây dựng hoặc sửa đổi giao diện và trang, tạo thành phần, triển khai bố cục, đáp ứng yêu cầu truy cập WCAG, quản lý trạng thái, hoặc khi đầu ra cần trông và cảm nhận chất lượng sản xuất thay vì do AI tạo ra.
developmentdesign
security-and-hardening
addyosmani
Tăng cường bảo mật mã nguồn chống lại các lỗ hổng. Sử dụng khi xử lý đầu vào người dùng, xác thực, lưu trữ dữ liệu hoặc tích hợp bên ngoài. Sử dụng khi xây dựng bất kỳ tính năng nào chấp nhận dữ liệu không đáng tin cậy, quản lý phiên người dùng hoặc tương tác với dịch vụ bên thứ ba.
spec-driven-development
addyosmani
Tạo đặc tả trước khi viết mã. Sử dụng khi bắt đầu một dự án mới, tính năng mới hoặc thay đổi quan trọng và chưa có đặc tả nào tồn tại. Sử dụng khi yêu cầu chưa rõ ràng, mơ hồ hoặc chỉ tồn tại dưới dạng ý tưởng mơ hồ.
developmentdocumentproject-management