性能优化

91 阅读5分钟

性能优化

一、常用指标

  1. FP (First Paint,首次绘制)
  • 含义:浏览器第一次把任何像素绘制到屏幕上的时间点(可能只是背景色)。
  • 作用:衡量用户从请求页面到浏览器首次有“反应”的时间。
  1. FCP (First Contentful Paint,首次内容绘制)
  • 含义:页面首次渲染出文字、图片、SVG 等内容。
  • 作用:用户真正开始“感知到页面加载”。

好标准:FCP < 1.8s(Google Web Vitals 推荐)。

  1. LCP (Largest Contentful Paint,最大内容绘制)

    • 含义:页面中最大的一块可见内容**(通常是大图或大标题)绘制完成的时间点。
    • 特点:代表页面主体内容完成渲染。
    • 好标准:LCP < 2.5s。

二、当前网站指标

  1. 网络层

    • TCP连接耗时过高(220.63ms),远超理想值(<100ms)
    • SSL建连时间(108.64ms)和TTFB首字节响应(119.75ms)显著高于优化目标(<50ms)
  2. 渲染性能严重延迟

    • DOM解析时间高达 1427.78ms(理想值应<500ms)
    • FP首次绘制(4954.86ms)和FCP首次内容绘制(5654.03ms)接近5-6秒,远超用户可接受范围(<2秒)
    • LCP最大内容绘制延迟到 6376.20ms,导致用户长时间看不到主要内容
  3. 资源加载阻塞严重

    • 资源加载耗时 5619.96ms,说明资源体积过大或请求过多
    • domReady时间 4541.14ms 表明JS执行/DOM操作存在瓶颈
  4. 可交互性较差

    • 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-MatchIf-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

6. Service Worker