Appearance
网页性能优化指标与实践
目录
1. 加载性能指标
1.1 首字节时间 (TTFB - Time To First Byte)
指标说明
TTFB是指从浏览器发起请求到接收到第一个字节的时间,包括DNS解析、TCP连接、SSL握手、服务器处理等时间。TTFB过长通常表示服务器响应慢或网络延迟高。
理想阈值: ≤800ms
优化案例
案例1:CDN加速配置
nginx
# Nginx CDN回源配置
upstream origin {
server origin.example.com:443;
}
server {
listen 443 ssl http2;
server_name cdn.example.com;
# HTTP/2 多路复用
http2_push_preload on;
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
proxy_pass https://origin;
proxy_cache_valid 200 304 1y;
proxy_cache_key "$host$request_uri";
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
}
# HTML不缓存
location ~* \.html$ {
proxy_pass https://origin;
proxy_cache_valid 200 5m;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}案例2:Node.js Gzip压缩与响应优化
javascript
const express = require('express')
const compression = require('compression')
const helmet = require('helmet')
const app = express()
// 安全头配置
app.use(helmet())
// 智能压缩配置
app.use(
compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
const type = req.headers['accept-encoding']
if (!type) return false
if (req.headers['x-no-compression']) return false
return compression.filter(req, res)
}
})
)
// 数据库连接池优化
const mysql = require('mysql2/promise')
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'mydb',
waitForConnections: true,
connectionLimit: 20,
queueLimit: 0
})
// 查询缓存示例
const LRU = require('lru-cache')
const cache = new LRU({ max: 500, ttl: 60000 })
app.get('/api/data', async (req, res) => {
const cacheKey = `data:${req.query.id}`
if (cache.has(cacheKey)) {
return res.json(cache.get(cacheKey))
}
const [rows] = await pool.execute('SELECT * FROM data WHERE id = ?', [
req.query.id
])
cache.set(cacheKey, rows)
res.json(rows)
})案例3:Redis缓存与数据库优化
sql
-- 添加复合索引
CREATE INDEX idx_user_created_status ON users(created_at, status);
-- 查询优化
EXPLAIN ANALYZE
SELECT * FROM users
WHERE created_at > '2024-01-01'
AND status = 'active';
-- 分区表(大数据量场景)
CREATE TABLE orders (
id INT,
order_date DATE,
amount DECIMAL(10,2)
)
PARTITION BY RANGE (TO_DAYS(order_date)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01'))
);javascript
// Redis 缓存示例
const redis = require('redis')
const client = redis.createClient()
async function getProductData(productId) {
const cacheKey = `product:${productId}`
let data = await client.get(cacheKey)
if (data) {
return JSON.parse(data)
}
data = await db.query('SELECT * FROM products WHERE id = ?', [productId])
await client.setEx(cacheKey, 3600, JSON.stringify(data))
return data
}1.2 首次内容绘制 (FCP - First Contentful Paint)
指标说明
FCP是指从页面加载开始到浏览器渲染出第一个DOM元素(文本、图片、canvas等)的时间。它标志着用户开始看到页面内容。
理想阈值: ≤1.8s
优化案例
案例1:内联关键CSS
html
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 内联首屏关键CSS -->
<style>
/* Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 字体系统 */
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui,
sans-serif;
line-height: 1.5;
color: #333;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 100;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
/* Hero区域 */
.hero {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 2rem;
background: #f8fafc;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
}
.hero-subtitle {
font-size: 1.25rem;
color: #64748b;
margin-bottom: 2rem;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
</style>
<!-- 预加载关键资源 -->
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="preload" href="/images/hero-bg.jpg" as="image" />
<!-- 非关键CSS延迟加载 -->
<link
rel="preload"
href="/styles/main.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/styles/main.css" /></noscript>
</head>案例2:资源预加载优化
html
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
<link rel="dns-prefetch" href="//analytics.example.com" />
<!-- 预连接 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://api.example.com" />
<!-- 预加载 -->
<link rel="preload" href="/critical.js" as="script" />
<link rel="preload" href="/hero.webp" as="image" />
<link rel="preload" href="/styles/critical.css" as="style" />
<!-- 预获取(用于下一页可能需要的资源) -->
<link rel="prefetch" href="/page2.html" as="document" />
<link rel="prefetch" href="/page2-styles.css" as="style" />案例3:Web字体优化
css
/* 字体加载优化 */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url('/fonts/inter-regular.woff2') format('woff2'),
url('/fonts/inter-regular.woff') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/inter-semibold.woff2') format('woff2');
}
/* 字体子集化 - 只包含所需字符 */
@font-face {
font-family: 'IconFont';
font-display: block;
src: url('/fonts/icons.woff2') format('woff2');
unicode-range: U+F000-F0FF;
}javascript
// 字体加载监控
if ('fonts' in document) {
document.fonts.ready.then(function () {
document.documentElement.classList.add('fonts-loaded')
})
}1.3 最大内容绘制 (LCP - Largest Contentful Paint)
指标说明
LCP是指视口中最大的内容元素(通常是图片或文本块)完成渲染的时间,是Core Web Vitals的核心指标之一,直接影响用户对页面加载速度的感知。
理想阈值: ≤2.5s 需要改进: 2.5s - 4s 较差: >4s
优化案例
案例1:响应式图片与现代格式
html
<!-- 使用srcset和sizes提供响应式图片 -->
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-1600.jpg 1600w
"
sizes="(max-width: 480px) 400px,
(max-width: 768px) 800px,
(max-width: 1200px) 1200px,
1600px"
alt="Hero banner"
loading="eager"
fetchpriority="high"
width="1600"
height="900"
/>
<!-- 使用picture元素支持多种格式 -->
<picture>
<source
media="(min-width: 1200px)"
srcset="hero-large.webp 1x, hero-large@2x.webp 2x"
type="image/webp"
/>
<source
media="(min-width: 768px)"
srcset="hero-medium.webp 1x, hero-medium@2x.webp 2x"
type="image/webp"
/>
<source
srcset="hero-small.webp 1x, hero-small@2x.webp 2x"
type="image/webp"
/>
<source
media="(min-width: 1200px)"
srcset="hero-large.jpg 1x, hero-large@2x.jpg 2x"
type="image/jpeg"
/>
<img
src="hero-small.jpg"
alt="Hero banner"
loading="eager"
fetchpriority="high"
width="800"
height="450"
/>
</picture>案例2:Next.js Image组件优化
jsx
import Image from 'next/image'
function Hero() {
return (
<section className='hero'>
<Image
src='/images/hero.jpg'
alt='Hero banner'
fill
priority
placeholder='blur'
blurDataURL='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...'
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
quality={85}
/>
<div className='hero-content'>
<h1>Welcome to Our Site</h1>
<p>Discover amazing products</p>
</div>
</section>
)
}案例3:SSR/SSG优化LCP内容
javascript
// Next.js 服务端渲染
export async function getServerSideProps(context) {
const [productRes, bannerRes] = await Promise.all([
fetch('https://api.example.com/products/featured'),
fetch('https://api.example.com/banners/home')
])
const product = await productRes.json()
const banner = await bannerRes.json()
return {
props: {
product,
banner
}
}
}
export default function Home({ product, banner }) {
return (
<main>
<section className='banner'>
<img
src={banner.image}
alt={banner.title}
width={banner.width}
height={banner.height}
loading='eager'
/>
<h1>{banner.title}</h1>
</section>
<section className='featured-product'>
<h2>Featured Product</h2>
<h3>{product.name}</h3>
<p>{product.description}</p>
</section>
</main>
)
}1.4 首屏加载时间
指标说明
首屏加载时间是指页面首屏内容(用户无需滚动即可看到的内容)全部加载完成的时间,是用户感知最明显的指标之一。
理想阈值: ≤3s
优化案例
案例1:React代码分割与懒加载
jsx
import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
// 首屏组件直接导入
import Header from './components/Header'
import Hero from './components/Hero'
import Footer from './components/Footer'
// 非首屏组件懒加载
const ProductList = lazy(() => import('./components/ProductList'))
const FeatureSection = lazy(() => import('./components/FeatureSection'))
const Testimonials = lazy(() => import('./components/Testimonials'))
const BlogSection = lazy(() => import('./components/BlogSection'))
// 加载占位组件
function LoadingSkeleton() {
return (
<div className='loading-skeleton'>
<div className='skeleton-header'></div>
<div className='skeleton-grid'>
{[1, 2, 3].map(i => (
<div key={i} className='skeleton-card'></div>
))}
</div>
</div>
)
}
function App() {
return (
<Router>
<Header />
<Hero />
<Suspense fallback={<LoadingSkeleton />}>
<ProductList />
</Suspense>
<Suspense fallback={<LoadingSkeleton />}>
<FeatureSection />
</Suspense>
<Suspense fallback={<LoadingSkeleton />}>
<Testimonials />
</Suspense>
<Suspense fallback={<LoadingSkeleton />}>
<BlogSection />
</Suspense>
<Footer />
</Router>
)
}
export default App案例2:图片懒加载实现
html
<!-- 原生懒加载(现代浏览器支持) -->
<img
src="product-1.jpg"
loading="lazy"
alt="Product 1"
width="400"
height="400"
/>
<!-- Intersection Observer 懒加载(兼容性更好) -->
<img
data-src="product-2.jpg"
class="lazy"
alt="Product 2"
width="400"
height="400"
/>
<script>
document.addEventListener('DOMContentLoaded', function () {
const lazyImages = document.querySelectorAll('img.lazy')
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.onload = function () {
img.classList.add('loaded')
}
observer.unobserve(img)
}
})
},
{
rootMargin: '200px 0px',
threshold: 0.01
}
)
lazyImages.forEach(img => {
imageObserver.observe(img)
})
} else {
lazyImages.forEach(img => {
img.src = img.dataset.src
})
}
})
</script>
<style>
img.lazy {
background: #f0f0f0;
min-height: 200px;
transition: opacity 0.3s;
opacity: 0;
}
img.lazy.loaded {
opacity: 1;
}
</style>案例3:骨架屏实现
html
<!-- 骨架屏组件 -->
<div class="product-card skeleton">
<div class="skeleton-image"></div>
<div class="skeleton-content">
<div class="skeleton-title"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text"></div>
<div class="skeleton-price"></div>
</div>
</div>
<style>
/* 骨架屏动画 */
@keyframes skeleton-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.skeleton {
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
.skeleton-image {
width: 100%;
height: 200px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 8px;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.skeleton-title {
height: 24px;
width: 80%;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 12px;
}
.skeleton-text {
height: 16px;
width: 100%;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-price {
height: 20px;
width: 40%;
background: #f0f0f0;
border-radius: 4px;
margin-top: 16px;
}
</style>1.5 DOMContentLoaded时间
指标说明
DOMContentLoaded事件在初始HTML文档被完全加载和解析后触发,不等待样式表、图片和子框架加载完成。
理想阈值: ≤2s
优化案例
案例1:JavaScript异步加载策略
html
<!-- async - 异步加载,下载完立即执行,不保证顺序 -->
<script src="analytics.js" async></script>
<!-- defer - 延迟加载,在DOMContentLoaded前按顺序执行 -->
<script src="lib.js" defer></script>
<script src="app.js" defer></script>
<!-- 动态加载脚本 -->
<script>
document.addEventListener('DOMContentLoaded', function () {
function loadScript(src, callback) {
const script = document.createElement('script')
script.src = src
script.onload = callback
document.body.appendChild(script)
}
loadScript('analytics.js')
loadScript('chat-widget.js')
loadScript('heatmap.js')
})
</script>
<!-- type="module" 自动延迟执行 -->
<script type="module" src="app.js"></script>案例2:CSS加载优化
html
<head>
<!-- 关键CSS内联 -->
<style>
body {
font-family: system-ui;
margin: 0;
}
.header {
background: #333;
color: white;
padding: 1rem;
}
</style>
<!-- 首屏CSS -->
<link rel="stylesheet" href="critical.css" media="all" />
<!-- 非首屏CSS - 使用media="print" trick -->
<link
rel="stylesheet"
href="non-critical.css"
media="print"
onload="this.media='all'"
/>
<noscript><link rel="stylesheet" href="non-critical.css" /></noscript>
<!-- 预加载方式 -->
<link
rel="preload"
href="styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="styles.css" /></noscript>
</head>1.6 Load时间
指标说明
Load事件在所有资源(包括图片、样式表、脚本、iframe等)都加载完成后触发。
理想阈值: ≤4s
优化案例
案例1:Webpack资源压缩配置
javascript
const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
}
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
}
]
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
format: {
comments: false
}
},
extractComments: false
}),
new CssMinimizerPlugin(),
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
jpeg: { quality: 80 },
png: { quality: 80 },
webp: { quality: 80 }
}
}
}
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
]
}2. 渲染性能指标
2.1 首次有意义绘制 (FMP - First Meaningful Paint)
指标说明
FMP是指页面主要内容出现在屏幕上的时间。虽然已被LCP替代,但仍有参考价值。
理想阈值: ≤2s
优化案例
案例1:Next.js服务端渲染
jsx
// pages/index.js
import React from 'react'
import Head from 'next/head'
export default function Home({ posts, featuredProduct }) {
return (
<div>
<Head>
<title>My E-commerce Site</title>
</Head>
<main>
<section className='hero'>
<h1>Welcome to Our Store</h1>
<img
src={featuredProduct.image}
alt={featuredProduct.name}
width={600}
height={400}
priority
/>
<h2>{featuredProduct.name}</h2>
<p>{featuredProduct.description}</p>
</section>
<section className='blog-posts'>
<h2>Latest Posts</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</article>
))}
</section>
</main>
</div>
)
}
export async function getServerSideProps() {
const [postsRes, productRes] = await Promise.all([
fetch('https://api.example.com/posts?limit=3'),
fetch('https://api.example.com/products/featured')
])
const posts = await postsRes.json()
const featuredProduct = await productRes.json()
return {
props: {
posts,
featuredProduct
}
}
}案例2:静态生成 (SSG)
jsx
// pages/products/[id].js
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/products')
const products = await res.json()
const paths = products.map(product => ({
params: { id: product.id.toString() }
}))
return { paths, fallback: 'blocking' }
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/products/${params.id}`)
const product = await res.json()
return {
props: { product },
revalidate: 60
}
}2.2 可交互时间 (TTI - Time To Interactive)
指标说明
TTI是指页面变得完全可交互的时间,即主线程长期空闲,用户输入能得到及时响应。
理想阈值: ≤3s
优化案例
案例1:Web Worker处理耗时任务
javascript
// worker.js - 单独文件
self.onmessage = function (e) {
const { type, data } = e.data
switch (type) {
case 'PROCESS_DATA':
const result = heavyComputation(data)
self.postMessage({ type: 'DATA_PROCESSED', result })
break
case 'SORT_ARRAY':
const sorted = data.sort((a, b) => a.value - b.value)
self.postMessage({ type: 'ARRAY_SORTED', sorted })
break
}
}
function heavyComputation(data) {
let result = 0
for (let i = 0; i < 10000000; i++) {
result += Math.sqrt(data * i)
}
return result
}
// main.js
class DataProcessor {
constructor() {
this.worker = new Worker('/worker.js')
this.callbacks = new Map()
this.callbackId = 0
this.worker.onmessage = e => {
const { callbackId, ...result } = e.data
const callback = this.callbacks.get(callbackId)
if (callback) {
callback(result)
this.callbacks.delete(callbackId)
}
}
}
process(data, callback) {
const id = this.callbackId++
this.callbacks.set(id, callback)
this.worker.postMessage({ type: 'PROCESS_DATA', data, callbackId: id })
}
}
const processor = new DataProcessor()
processor.process(100, result => {
console.log('处理完成:', result)
})案例2:使用requestIdleCallback延迟非关键任务
javascript
function scheduleIdleTasks() {
if ('requestIdleCallback' in window) {
requestIdleCallback(
deadline => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift()
task()
}
if (tasks.length > 0) {
scheduleIdleTasks()
}
},
{ timeout: 2000 }
)
} else {
setTimeout(() => {
tasks.forEach(task => task())
}, 1000)
}
}
const tasks = [
() => loadAnalytics(),
() => loadChatWidget(),
() => loadThirdPartyAds(),
() => prefetchNextPageData(),
() => initializeNonCriticalComponents()
]
window.addEventListener('load', () => {
scheduleIdleTasks()
})案例3:长任务拆分
javascript
function processLargeArray(array, processItem, onComplete) {
const chunkSize = 100
let index = 0
const results = []
function processChunk() {
const end = Math.min(index + chunkSize, array.length)
for (let i = index; i < end; i++) {
results.push(processItem(array[i], i))
}
index = end
if (index < array.length) {
requestAnimationFrame(processChunk)
} else {
onComplete(results)
}
}
processChunk()
}
const largeArray = Array.from({ length: 10000 }, (_, i) => i)
processLargeArray(
largeArray,
item => item * 2,
results => {
console.log('处理完成,结果数量:', results.length)
}
)2.3 总阻塞时间 (TBT - Total Blocking Time)
指标说明
TBT是FCP和TTI之间所有长任务(超过50ms)的阻塞时间总和。长任务超过50ms的部分算作阻塞时间。
理想阈值: ≤200ms
优化案例
案例1:动态导入与代码分割
javascript
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
},
optimization: {
splitChunks: {
chunks: 'all',
maxInitialRequests: 25,
maxAsyncRequests: 25,
minSize: 20000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20
},
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/,
name: 'lodash',
chunks: 'all',
priority: 10
}
}
}
}
}
// 组件中动态导入
function ChartComponent() {
const [Chart, setChart] = React.useState(null)
React.useEffect(() => {
import('d3').then(d3 => {
setChart(() => d3)
})
}, [])
if (!Chart) {
return <div>Loading chart...</div>
}
return <div>Chart rendered with D3</div>
}案例2:React性能优化
jsx
import React, { memo, useMemo, useCallback, useState } from 'react'
const ExpensiveListItem = memo(function ExpensiveListItem({ item, onClick }) {
console.log('ExpensiveListItem rendered')
return <li onClick={() => onClick(item.id)}>{item.name}</li>
})
function App() {
const [items, setItems] = useState([])
const [count, setCount] = useState(0)
const sortedItems = useMemo(() => {
console.log('Sorting items...')
return [...items].sort((a, b) => a.name.localeCompare(b.name))
}, [items])
const handleItemClick = useCallback(id => {
console.log('Item clicked:', id)
}, [])
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ul>
{sortedItems.map(item => (
<ExpensiveListItem
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</ul>
</div>
)
}2.4 帧率 (FPS - Frames Per Second)
指标说明
帧率是指每秒渲染的帧数,理想值是60fps(每帧16.67ms),最低应保持30fps以上以保证流畅。
理想阈值: ≥60fps
优化案例
案例1:使用transform和opacity进行动画
css
/* 好的做法:使用transform和opacity */
.slide-in {
transform: translateX(0);
opacity: 1;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.slide-out {
transform: translateX(-100%);
opacity: 0;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
/* GPU加速提示 */
.gpu-accelerated {
transform: translateZ(0);
backface-visibility: hidden;
will-change: transform, opacity;
}
/* 不好的做法:使用top/left/width等触发布局重排 */
.bad-animation {
position: relative;
left: 0;
transition: left 0.3s ease;
}案例2:requestAnimationFrame动画
javascript
function smoothAnimation() {
let start = null
const element = document.querySelector('.animate-me')
const duration = 1000
function step(timestamp) {
if (!start) start = timestamp
const progress = Math.min((timestamp - start) / duration, 1)
const easeOutCubic = 1 - Math.pow(1 - progress, 3)
const translateX = easeOutCubic * 300
element.style.transform = `translateX(${translateX}px)`
if (progress < 1) {
requestAnimationFrame(step)
}
}
requestAnimationFrame(step)
}
// 滚动性能优化
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateScrollPosition()
ticking = false
})
ticking = true
}
})
function updateScrollPosition() {
const scrollY = window.scrollY
const header = document.querySelector('.header')
if (scrollY > 100) {
header.classList.add('header--scrolled')
} else {
header.classList.remove('header--scrolled')
}
}案例3:CSS will-change优化
css
/* 提前告知浏览器哪些属性会变化 */
.animated-element {
will-change: transform, opacity;
}
/* 为频繁滚动的元素启用 */
.scroll-container {
will-change: scroll-position;
overflow-y: auto;
}3. 交互性能指标
3.1 首次输入延迟 (FID - First Input Delay)
指标说明
FID是指用户首次与页面交互(点击、触摸等)到浏览器实际响应的时间。已被INP替代。
理想阈值: ≤100ms
优化案例
案例1:减少事件监听开销
javascript
// 使用事件委托
document.addEventListener(
'click',
e => {
const button = e.target.closest('.button')
if (button) {
handleButtonClick(button)
}
const link = e.target.closest('.nav-link')
if (link) {
handleNavigation(link)
}
},
{ passive: true }
)
// 使用passive监听器
document.addEventListener('touchstart', handleTouchStart, { passive: true })
document.addEventListener('touchmove', handleTouchMove, { passive: true })
// 移除不需要的事件监听器
function Component() {
const [data, setData] = useState(null)
useEffect(() => {
const handleScroll = () => {
console.log('Scrolling')
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return <div>{data}</div>
}3.2 交互到下一步绘制 (INP - Interaction to Next Paint)
指标说明
INP是指从用户交互到下一帧绘制的时间,是替代FID的新指标,考虑所有交互而非仅首次。
理想阈值: ≤200ms 需要改进: 200ms - 500ms 较差: >500ms
优化案例
案例1:分阶段响应交互
javascript
async function handleCheckoutClick(e) {
e.preventDefault()
const startTime = performance.now()
const button = e.currentTarget
button.disabled = true
button.textContent = 'Processing...'
await new Promise(resolve => requestAnimationFrame(resolve))
try {
const cartData = getCartData()
const [orderRes, inventoryRes] = await Promise.all([
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(cartData)
}),
fetch('/api/inventory/reserve', {
method: 'POST',
body: JSON.stringify(cartData)
})
])
const order = await orderRes.json()
button.textContent = 'Success!'
button.classList.add('success')
setTimeout(() => {
window.location.href = `/order-confirmation/${order.id}`
}, 500)
} catch (error) {
button.disabled = false
button.textContent = 'Try Again'
showError(error.message)
}
const endTime = performance.now()
console.log(`交互处理时间: ${endTime - startTime}ms`)
}案例2:防抖和节流
javascript
function debounce(func, wait = 300) {
let timeout
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, args), wait)
}
}
function throttle(func, limit = 100) {
let inThrottle
return function (...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
const searchInput = document.querySelector('#search')
searchInput.addEventListener(
'input',
debounce(e => {
performSearch(e.target.value)
}, 300)
)
const resizeHandler = throttle(() => {
updateLayout()
}, 100)
window.addEventListener('resize', resizeHandler)3.3 累积布局偏移 (CLS - Cumulative Layout Shift)
指标说明
CLS测量整个页面生命周期中发生的所有意外布局偏移的分数总和。
理想阈值: ≤0.1 需要改进: 0.1 - 0.25 较差: >0.25
优化案例
案例1:为媒体元素预留空间
html
<!-- 为图片设置width和height -->
<img
src="product.jpg"
alt="Product"
width="600"
height="400"
style="width: 100%; height: auto;"
/>
<!-- 使用aspect-ratio -->
<div class="image-container">
<img src="product.jpg" alt="Product" />
</div>
<style>
.image-container {
aspect-ratio: 16 / 9;
width: 100%;
background: #f0f0f0;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
<!-- 广告位预留 -->
<div class="ad-slot" style="min-height: 250px; min-width: 300px;">
<div id="ad-container"></div>
</div>案例2:避免动态内容插入
html
<!-- 横幅提示使用固定位置 -->
<div
class="banner"
style="position: fixed; top: 0; left: 0; right: 0; z-index: 1000;"
>
Important announcement!
</div>
<!-- 为字体设置fallback -->
<style>
body {
font-family:
'MyWebFont',
-apple-system,
BlinkMacSystemFont,
sans-serif;
}
@font-face {
font-family: 'MyWebFont';
font-display: swap;
src: url('myfont.woff2') format('woff2');
}
</style>4. 资源性能指标
4.1 资源加载时间
指标说明
单个资源从请求到加载完成的时间。
优化案例
案例1:HTTP/2与Server Push
nginx
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
http2_push_preload on;
location / {
root /var/www/html;
index index.html;
add_header Link "</styles.css>; rel=preload; as=style";
add_header Link "</script.js>; rel=preload; as=script";
}
}4.2 资源体积
指标说明
所有资源的总大小。
优化案例
案例1:图片优化
javascript
// 使用sharp进行图片处理
const sharp = require('sharp')
const fs = require('fs')
async function optimizeImage(inputPath, outputPath) {
const image = sharp(inputPath)
const metadata = await image.metadata()
let pipeline = image
if (metadata.width > 1920) {
pipeline = pipeline.resize(1920)
}
await Promise.all([
pipeline.clone().webp({ quality: 80 }).toFile(`${outputPath}.webp`),
pipeline
.clone()
.jpeg({ quality: 85, mozjpeg: true })
.toFile(`${outputPath}.jpg`),
pipeline.clone().avif({ quality: 70 }).toFile(`${outputPath}.avif`)
])
}案例2:Tree Shaking
javascript
// 导入单个模块而非整个库
import { debounce } from 'lodash-es'
import { format, parseISO } from 'date-fns'
// webpack配置
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: false
}
}4.3 缓存命中率
指标说明
从缓存中加载的资源占总资源的比例。
理想阈值: ≥90%
优化案例
案例1:Nginx缓存配置
nginx
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
server {
location / {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 1h;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status;
}
}案例2:Service Worker缓存
javascript
const CACHE_NAME = 'my-cache-v1'
const ASSETS = ['/', '/styles.css', '/script.js', '/favicon.ico']
self.addEventListener('install', event => {
event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)))
})
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response
}
return fetch(event.request)
})
)
})5. Web Vitals指标
Web Vitals是Google定义的用户体验核心指标:
| 指标 | 良好阈值 | 需要改进 | 较差 |
|---|---|---|---|
| LCP | ≤2.5s | 2.5s-4s | >4s |
| FID | ≤100ms | 100ms-300ms | >300ms |
| CLS | ≤0.1 | 0.1-0.25 | >0.25 |
| INP | ≤200ms | 200ms-500ms | >500ms |
| TTFB | ≤800ms | 800ms-1800ms | >1800ms |
6. 优化案例
完整优化流程案例
优化前分析:
LCP: 4.2s
FID: 280ms
CLS: 0.4
TTFB: 1.2s
总资源大小: 3.8MB优化步骤:
图片优化
- 转换为WebP格式,体积减少60%
- 实现响应式图片
- 预加载LCP图片
JavaScript优化
- 代码分割,首屏JS减少70%
- Web Worker处理计算任务
- 延迟加载非关键脚本
CSS优化
- 内联关键CSS (约15KB)
- 延迟加载非关键CSS
- 移除未使用的CSS
服务器优化
- 启用CDN,TTFB降至300ms
- 配置缓存策略
- 启用Brotli压缩
布局优化
- 为图片预留空间
- 使用font-display: swap
- 固定定位横幅
优化后结果:
LCP: 1.8s (提升57%)
FID: 50ms (提升82%)
CLS: 0.05 (提升87%)
TTFB: 300ms (提升75%)
总资源大小: 1.2MB (减少68%)