性能优化
一、常用指标
- FP (First Paint,首次绘制)
- 含义:浏览器第一次把任何像素绘制到屏幕上的时间点(可能只是背景色)。
- 作用:衡量用户从请求页面到浏览器首次有“反应”的时间。
- FCP (First Contentful Paint,首次内容绘制)
- 含义:页面首次渲染出文字、图片、SVG 等内容。
- 作用:用户真正开始“感知到页面加载”。
好标准:FCP < 1.8s(Google Web Vitals 推荐)。
-
LCP (Largest Contentful Paint,最大内容绘制)
- 含义:页面中最大的一块可见内容**(通常是大图或大标题)绘制完成的时间点。
- 特点:代表页面主体内容完成渲染。
- 好标准:LCP < 2.5s。
二、当前网站指标
-
网络层
- TCP连接耗时过高(220.63ms),远超理想值(<100ms)
- SSL建连时间(108.64ms)和TTFB首字节响应(119.75ms)显著高于优化目标(<50ms)
-
渲染性能严重延迟
- DOM解析时间高达 1427.78ms(理想值应<500ms)
- FP首次绘制(4954.86ms)和FCP首次内容绘制(5654.03ms)接近5-6秒,远超用户可接受范围(<2秒)
- LCP最大内容绘制延迟到 6376.20ms,导致用户长时间看不到主要内容
-
资源加载阻塞严重
- 资源加载耗时 5619.96ms,说明资源体积过大或请求过多
- domReady时间 4541.14ms 表明JS执行/DOM操作存在瓶颈
-
可交互性较差
- TTI可交互时间 1997.67ms(应优化至<1.5秒)
三、优化措施
1. 图片懒加载
import React, { useEffect, useRef, useState } from "react";
// 懒加载图片组件
function LazyImage({ src, alt, placeholder }) {
// 1. 创建一个 ref,用来绑定 <img> DOM 元素
const imgRef = useRef(null);
// 2. 记录图片是否进入视口(true 表示已经进入,可以加载真实图片)
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// 3. 创建 IntersectionObserver,用来监听元素是否进入视口
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
// 如果图片进入视口
if (entry.isIntersecting) {
setIsVisible(true); // 更新状态为可见
observer.unobserve(entry.target); // 加载一次后就停止观察,避免重复触发
}
});
},
{ threshold: 0.1 } // 当图片至少有 10% 出现在视口时触发
);
// 4. 开始观察 <img> 元素
if (imgRef.current) {
observer.observe(imgRef.current);
}
// 5. 组件卸载时,断开 observer,避免内存泄漏
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef} // 绑定 ref,供 IntersectionObserver 使用
src={isVisible ? src : placeholder} // 如果可见就加载真实图片,否则显示占位图
alt={alt}
style={{
width: "100%",
height: "auto",
transition: "0.3s filter" // 图片切换时加过渡效果(可扩展成模糊过渡)
}}
/>
);
}
// 使用示例
function App() {
// 模拟一个图片数组(真实项目可能来自后端接口)
const images = [
{ src: "hero1.jpg", placeholder: "placeholder1.jpg", alt: "图片1" },
{ src: "hero2.jpg", placeholder: "placeholder2.jpg", alt: "图片2" },
{ src: "hero3.jpg", placeholder: "placeholder3.jpg", alt: "图片3" },
{ src: "hero4.jpg", placeholder: "placeholder4.jpg", alt: "图片4" },
{ src: "hero5.jpg", placeholder: "placeholder5.jpg", alt: "图片5" }
];
return (
<div>
<h1>React 多图片懒加载</h1>
{/* 遍历图片数组,渲染多个 LazyImage */}
{images.map((img, index) => (
<LazyImage
key={index} // React 列表需要 key
src={img.src} // 真实图片地址
placeholder={img.placeholder} // 占位图地址
alt={img.alt} // 描述
/>
))}
</div>
);
}
为什么不用 scroll 事件?
IntersectionObserver 是浏览器提供的 API,用来 异步观察某个元素和视口(或指定容器)之间的交叉情况。
- 它能告诉你:某个元素 是否进入了视口,以及 进入了多少(比例) 。
在 IntersectionObserver 出现之前,常见做法是监听 scroll 事件 + getBoundingClientRect() 来判断元素位置。 缺点:
- 需要频繁计算元素位置 → 性能开销大
- 滚动过快时容易漏判
- 手动实现逻辑复杂
而 IntersectionObserver 是由浏览器底层优化的:
- 异步执行,不会阻塞主线程
- 内部有优化机制(节流、批处理)
- 简单好用,只需配置
threshold(可见比例阈值)
2. CDN存放静态资源
- 分布式服务器:CDN (内容分发网络)在全球各地部署节点服务器,用户访问时可以从离自己最近的节点获取资源;
- 减轻源服务器压力:用户请求被 CDN 节点拦截和处理,源服务器只处理必要的动态请求,避免高并发访问导致服务器过载。
- 提高并发和吞吐量:浏览器对同一域名的并发连接有限,将静态资源放在 CDN 不同域名上,可以并行加载更多资源,提高整体加载速度。
3. 浏览器缓存
(1)HTTP 缓存头
-
Cache-Control(推荐,强缓存,浏览器直接使用缓存,不请求服务器),Expires(HTTP/1.0 老方式)max-age=xxx:资源在本地缓存多久(秒)public/private:是否可共享缓存no-cache/no-store:不缓存或必须验证后再使用 示例:
Cache-Control: public, max-age=31536000 -
ETag/Last-Modified(协商缓存) 浏览器请求时,会带上If-None-Match或If-Modified-Since,服务端判断资源是否变化,不变化就返回 304,不传输资源体。
(2)静态资源版本化
-
CSS、JS、图片等资源加哈希名:
main.abc123.js style.efg456.css -
改变文件内容时,hash 改变,保证更新时浏览器重新加载,否则直接走缓存。
Webpack 示例
在 Webpack 配置中开启 content hash:
output: {
filename: 'js/[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
contenthash→ 文件内容改变,hash 会改变,文件名改变- 配合服务器设置长缓存即可安全使用
Vite / Rollup
-
Vite 默认生成带 hash 的文件:
dist/assets/index-abc123.js dist/assets/style-xyz456.css -
服务器同样配置
Cache-Control: max-age=31536000, immutable即可
4. 代码分割
// webpack.config.js
optimization: {
splitChunks: {
// 指定对哪些 chunk 进行分割
// 'all' 表示对同步和异步(动态 import)引入的模块都进行代码分割
chunks: 'all',
// 缓存组,用来定义如何抽取和分组模块
cacheGroups: {
// 定义一个名为 vendor 的分组
vendor: {
// 匹配条件:所有在 node_modules 目录下的模块(第三方依赖)
test: /[\/]node_modules[\/]/,
// 打包生成的 chunk 名称,这里会输出 vendors.js(或 vendors.[hash].js)
name: 'vendors',
// 对同步和异步代码中引入的 node_modules 模块都进行分割
chunks: 'all'
}
}
}
}
这样打包后的效果就是:
- 业务代码会在
main.js(或对应入口 bundle)里。 - 第三方依赖会被统一抽离到
vendors.js,便于浏览器缓存和复用。
第三方库通常更新频率低,可以长期缓存 vendors.js。下次只更新业务代码时,浏览器直接复用缓存的 vendor 包。
5. 服务端渲染
React 技术栈
-
使用 Next.js
getServerSideProps→ 服务端渲染页面getStaticProps→ 静态生成页面并配合 CDN 缓存- 内置 Automatic Code Splitting,只加载当前页面需要的 JS
Vue 技术栈
-
使用 Nuxt.js
- 内置 SSR、静态化、路由预加载
- 支持 hybrid 模式:部分页面用 SSR,部分页面用 CSR