Notes

资源与 Bundle 优化:代码分割、图片与 CDN

一句话结论:通过精细化的代码分割减小初始 JS 体积,实施现代化的图片格式与加载策略,并结合 CDN 全球缓存,是从根本上提升页面加载性能(尤其是 LCP)的关键工程手段。


1. JavaScript Bundle 优化

巨大的 JS bundle 是拖慢页面加载和交互的主要元凶。浏览器需要下载、解析、编译和执行这些脚本,这个过程会长时间阻塞主线程。

A. 代码分割 (Code Splitting)

代码分割是将一个巨大的 JS bundle 拆分成多个小块(chunks)的技术。用户在首次访问页面时,只加载当前页面必需的核心代码。其他代码(例如,非首屏组件、次要路由、特定功能的库)则按需加载。

实现方式:

  1. 基于路由的分割:最常见也最有效的方式。每个页面(路由)对应一个独立的 JS chunk。当用户导航到新页面时,才加载该页面的代码。现代框架如 Next.js 和 TanStack Router 都内置了此功能。
    // 使用 React.lazy 和 Suspense
    import { lazy, Suspense } from 'react';
    
    const AdminPage = lazy(() => import('./pages/AdminPage')); // AdminPage.js 会被打包成一个独立的 chunk
    const HomePage = lazy(() => import('./pages/HomePage'));
    
    function App() {
      return (
        <Router>
          <Suspense fallback={<div>Loading page...</div>}>
            <Switch>
              <Route path="/admin" component={AdminPage} />
              <Route path="/" component={HomePage} />
            </Switch>
          </Suspense>
        </Router>
      );
    }
    
  2. 基于组件的分割:对于一些重量级但非首屏关键的组件(如复杂的图表库、弹窗、评论区),也可以进行代码分割。
    const HeavyChart = lazy(() => import('./components/HeavyChart'));
    
    function Dashboard() {
      const [showChart, setShowChart] = useState(false);
    
      return (
        <div>
          <button onClick={() => setShowChart(true)}>Show Chart</button>
          {showChart && (
            <Suspense fallback={<div>Loading chart...</div>}>
              <HeavyChart />
            </Suspense>
          )}
        </div>
      );
    }
    

B. Bundle 分析与优化

要知道优化什么,首先得知道你的 bundle 里有什么。

  • 工具webpack-bundle-analyzervite-plugin-visualizer
  • 分析:运行这些工具会生成一个矩形树图,清晰地展示出每个模块在最终 bundle 中所占的大小。
  • 常见问题与对策
    • 巨大的库:某个库(如 lodash, moment.js)是否被完整引入?尝试只引入需要的模块(import get from 'lodash/get'),或者寻找更轻量的替代品(如用 date-fns 替代 moment.js)。
    • 重复的依赖:同一个库的不同版本是否被多次打包?检查你的 package-lock.jsonyarn.lock,尝试使用 npm dedupe 或依赖覆盖 (overrides) 来统一版本。
    • 非必要的 polyfills:是否为现代浏览器加载了过多的 polyfills?调整 babel@vitejs/plugin-legacy 的配置,使其根据目标浏览器按需引入。

2. 图片优化

图片是 LCP 指标的最大影响因素之一。未经优化的图片会严重拖慢页面加载。

A. 选择正确的格式

格式优点缺点适用场景
WebP全能选手。压缩率极高,支持有损/无损、动图和透明度。兼容性问题(基本已解决)。首选格式。几乎可以替代所有 PNG/JPEG。
AVIF下一代王者。压缩率比 WebP 更高,尤其是对于高保真图像。编码耗时较长,兼容性略低于 WebP。对质量要求极高的场景(摄影、艺术品)。
JPEG压缩率高,兼容性好。不支持透明度,有损压缩。照片、色彩丰富的图片。
PNG无损压缩,支持透明度。文件体积较大。Logo、图标、需要透明背景的图片。
SVG矢量格式,无限缩放,体积小。不适合复杂照片。Logo、图标、简单的几何图形。

策略:使用 <picture> 元素提供多种格式,让浏览器自己选择最优解。

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpeg" alt="description">
</picture>

B. 响应式图片与懒加载

  1. 响应式图片 (Responsive Images):根据不同的屏幕尺寸(设备像素比)提供不同分辨率的图片。避免在手机上加载桌面端的高清大图。使用 srcsetsizes 属性实现。
    <img srcset="image-480w.jpg 480w,
                 image-800w.jpg 800w"
         sizes="(max-width: 600px) 480px,
                800px"
         src="image-800w.jpg" alt="description">
    
  2. 懒加载 (Lazy Loading):只加载视口内或即将进入视口的图片。对于首屏以下的图片,这是必须实施的优化。现代浏览器已原生支持。
    <img src="image.jpg" loading="lazy" alt="description" width="200" height="200">
    

    注意不要对 LCP 元素使用 loading="lazy",这会延迟其加载,反而降低性能。

C. 图片 CDN

专业的图片 CDN (如 Cloudinary, Imgix) 能提供自动化、即时的图片优化服务:

  • 自动格式转换:根据浏览器的 Accept 头,自动提供 WebP/AVIF 格式。
  • 即时变换:通过 URL 参数实时调整图片尺寸、裁剪、质量和水印。
  • 全球分发:利用 CDN 网络加速全球用户的访问。

3. CDN 缓存策略

CDN (Content Delivery Network) 将你的静态资源(JS, CSS, 图片, 字体)缓存到全球各地的边缘节点。当用户请求资源时,会从离他最近的节点返回,大大降低延迟。

关键 HTTP 缓存头 (Cache-Control)

这是你与 CDN 和浏览器沟通缓存行为的方式。

  • 对于带 hash 的静态资源 (e.g., main.a8f5b2.js):
    • Cache-Control: public, max-age=31536000, immutable
    • public:可以被任何缓存(浏览器、CDN)缓存。
    • max-age=31536000:缓存一年。
    • immutable:告诉浏览器这个文件永远不会变,不需要因为刷新而重新验证。
    • 效果:一次下载,永久使用,直到文件名改变。这是最强的缓存策略。
  • 对于 HTML 文档
    • Cache-Control: no-cacheCache-Control: public, max-age=0, must-revalidate
    • 不代表不缓存,而是代表“每次使用前都必须回源服务器验证资源是否过期”。
    • 验证机制:通过 ETagLast-Modified 头。如果服务器返回 304 Not Modified,CDN/浏览器就使用本地缓存,只消耗一个极小的验证请求,而不是重新下载整个 HTML。
    • 效果:保证用户总能获取最新的页面入口,同时在内容未变时利用缓存。
  • 对于需要频繁更新的 API 数据
    • Cache-Control: no-store
    • 效果:完全禁止缓存。适用于敏感或实时性要求极高的数据。

4. 关联阅读

cd ..