| 指标 | 含义 | 衡量内容 | 建议值 |
|---|
| LCP (Largest Contentful Paint) | 最大内容绘制 | 页面主要内容加载完成时间 | ≤ 2.5s |
| FID (First Input Delay) | 首次输入延迟 | 用户首次交互到响应的时间 | ≤ 100ms |
| CLS (Cumulative Layout Shift) | 累积布局偏移 | 页面内容意外移动的程度 | ≤ 0.1 |
| 指标 | 含义 | 说明 |
|---|
| TTFB | Time To First Byte | 服务器响应首字节时间,反映网络和服务器性能 |
| FP | First Paint | 首次绘制,浏览器渲染第一个像素 |
| FCP | First Contentful Paint | 首次内容绘制,渲染第一个文本/图片 |
| TBT | Total Blocking Time | 总阻塞时间,FCP 到 TTI 之间主线程被阻塞的总时间 |
| TTI | Time To Interactive | 页面完全可交互的时间 |
window.performance.getEntriesByType('navigation')
performance.getEntriesByType('paint')
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime, entry.duration)
}
})
observer.observe({ type: 'largest-contentful-paint', buffered: true })
observer.observe({ type: 'layout-shift', buffered: true })
observer.observe({ type: 'first-input', buffered: true })
- 合并资源:CSS/JS 文件合并(构建工具自动处理)
- 雪碧图:将多个小图标合并为一张图片
- 内联小资源:小图片转 Base64 或 Data URI(webpack/vite 自动处理)
- HTTP/2 多路复用:升级到 HTTP/2 后合并反而可能降低缓存粒度,需权衡
| 资源类型 | 压缩方式 | 效果 |
|---|
| HTML | 去除空格、注释 | 缩小 10-20% |
| CSS | CSS Nano / Lightning CSS | 缩小 30-50% |
| JS | Terser / esbuild / SWC | 缩小 60-70% |
| 图片 | WebP / AVIF / Sharp | 缩小 60-80% |
| 字体 | 子集化(仅含用到的字符) | 缩小 90%+ |
- 静态资源部署到 CDN,利用边缘节点加速
- 常用的 CDN 服务:Cloudflare、阿里云 CDN、七牛云、jsDelivr(开源)
- CDN 也提供 gzip/brotli 压缩和 HTTP/2 支持
# Nginx 配置 Gzip
gzip on
gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml
gzip_min_length 1024
gzip_vary on
# Brotli 压缩率更高(比 gzip 小 20-30%),但需要 Nginx 编译 brotli 模块
# 强缓存(不请求服务器)
Cache-Control: max-age=31536000, immutable # 带 hash 的资源文件
# 协商缓存(需要验证)
Cache-Control: no-cache
ETag: "file-hash"
# 不缓存
Cache-Control: no-store
# HTML 文档:每次都验证
Cache-Control: no-cache
<link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="preload" href="main.js" as="script">
<link rel="prefetch" href="/detail-page.js">
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<script src="critical.js"></script>
<script defer src="deferred.js"></script>
<script async src="async.js"></script>
defer vs async:defer 按顺序执行(HTML 解析完后),async 加载完就执行(顺序不确定)。
const routes = [
{ path: '/detail', component: () => import('./views/Detail.vue') }
]
const Detail = React.lazy(() => import('./Detail'))
import( './heavyChart.js')
| 方式 | 说明 | 适用场景 |
|---|
| CSR | 客户端渲染 | SPA 后台、工具类 |
| SSR | 服务端渲染,返回 HTML + 注水 | SEO 重要、首屏要求高的页 |
| SSG | 构建时生成静态 HTML | 博客、文档站 |
| ISR | 增量静态再生 | 内容频繁更新的站点 |
| 格式 | 特点 | 适用场景 |
|---|
| JPEG | 有损压缩,不支持透明 | 照片、复杂渐变图 |
| PNG | 无损压缩,支持透明 | 图标、截图、需要透明的图 |
| WebP | 有损/无损,支持透明 | 现代浏览器的首选(比 JPEG 小 25-35%) |
| AVIF | 压缩率更高 | 下一代格式(Chrome 支持) |
| SVG | 矢量图,无限缩放 | 图标、Logo、插画 |
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
loading="lazy"
decoding="async"
alt="示例图片"
/>
<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="示例图片">
</picture>
<img loading="lazy" src="photo.jpg" alt="懒加载图片" />
<img src="tiny-blur.jpg" alt="占位" style="filter: blur(10px)" />
在线工具:tinypng.com, squoosh.app
CLI:sharp (Node), imagemin, avif-cli
构建插件:vite-plugin-imagemin, webpack-image-minimizer
heavyTask()
function chunkedTask(items) {
const chunk = items.splice(0, 50)
chunk.forEach(process)
if (items.length > 0) {
requestIdleCallback(() => chunkedTask(items))
}
}
const worker = new Worker('heavy-worker.js')
worker.postMessage({ data })
worker.onmessage = (e) => console.log('结果', e.data)
- 避免频繁读写样式,使用 class 批量修改
- 使用
transform 和 opacity 做动画(触发合成层,不触发重排) - 对频繁动画的元素使用
will-change: transform 或 contain: layout - 避免使用
table 布局(table 的 reflow 开销更大) - 将需要多次读写的 DOM 元素脱离文档流(absolute/fixed)
for (let i = 0; i < 100; i++) {
el.style.left = i + 'px'
}
el.style.transform = `translateX(100px)`
document.querySelectorAll('li').forEach(el => {
el.addEventListener('click', handler)
})
document.querySelector('ul').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') handler(e)
})
function debounce(fn, delay = 300) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
function throttle(fn, interval = 200) {
let last = 0
return (...args) => {
const now = Date.now()
if (now - last >= interval) {
last = now
fn(...args)
}
}
}
window.addEventListener('resize', debounce(resizeHandler, 200))
window.addEventListener('scroll', throttle(scrollHandler, 100))
input.addEventListener('input', debounce(searchAPI, 500))
当渲染大量列表项(万级)时,只渲染可视区域的 DOM 节点:
常见虚拟列表库:vue-virtual-scroller、react-window、@tanstack/virtual
- 避免嵌套过深(最多 3-4 层)
- 避免使用通配选择器
* - 避免
#id .class 冗余选择器 - 使用 BEM 命名减少嵌套
.element {
transition: transform 0.3s ease;
will-change: transform;
}
将首屏需要的 CSS 内联到 HTML <head> 中,非关键 CSS 延迟加载:
<head>
<style>
.header { ... }
.hero { ... }
</style>
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>
@import url('reset.css');
@import url('theme.css');
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="theme.css">
.side-panel {
contain: layout style paint;
contain: strict;
}
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'MyIconFont';
src: url('iconfont-subset.woff2') format('woff2');
unicode-range: U+E601-E61E;
}
<link rel="preload" href="font.woff2" as="font" crossorigin>
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
chart: ['echarts'],
}
}
},
cssCodeSplit: true,
sourcemap: 'hidden',
}
})
npm install rollup-plugin-visualizer
npm install webpack-bundle-analyzer
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="preconnect" href="https://api.example.com">
<link rel="preload" href="critical.css" as="style">
<link rel="prefetch" href="/next-page.js">
<link rel="prerender" href="/next-page.html">
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'
precacheAndRoute(self.__WB_MANIFEST)
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate()
)
- 过深的 DOM 树影响渲染性能
- 建议:正文
<body> 内节点不超过 1500 个,深度不超过 32 层
function animate() {
updatePosition()
requestAnimationFrame(animate)
}
requestIdleCallback(() => {
prefetchData()
}, { timeout: 2000 })
- 虚拟列表(见第四部分)
- 分页加载
- 无限滚动 + 缓冲(渲染适量 DOM)
const canvas = document.getElementById('canvas')
const offscreen = canvas.transferControlToOffscreen()
const worker = new Worker('canvas-worker.js')
worker.postMessage({ canvas: offscreen }, [offscreen])
| 优化点 | 做法 |
|---|
| 首屏加载 | SSR / SSG,减少首屏 JS |
| 触摸事件 | touch 事件延迟 300ms → 设置 touch-action: manipulation |
| 图片自适应 | srcset + sizes 按设备加载 |
| 减少重排 | 使用 transform、opacity |
| 点击区域 | 按钮最小 44x44px |
| 渲染层 | 启用 GPU 加速(will-change) |
| 工具 | 用途 |
|---|
| Chrome DevTools → Performance | 录制和分析性能瓶颈 |
| Chrome DevTools → Lighthouse | 生成性能报告和优化建议 |
| Chrome DevTools → Coverage | 查看 CSS/JS 使用覆盖率(找出无用代码) |
| WebPageTest | 多地区/多设备性能测试 |
| PageSpeed Insights | 核心 Web 指标分析 |
| BundlePhobia | 分析 npm 包体积和加载时间 |
| Sonar | Web 性能监控平台 |