4. 资源与布局优化:让页面启动不再“卡脖子”

资源加载和布局计算,往往是用户感知“卡顿”的元凶。你想想看,用户点开一个页面,先看到白屏,然后图片一张张蹦出来,布局还来回跳动——这种体验,用户不跑才怪。

我个人习惯把资源优化和布局优化放在一起做。为什么?因为它们都直接影响首屏渲染。资源太大,网络传输慢;布局太复杂,浏览器计算慢。两者加起来,启动时间就失控了。

4.1 资源文件压缩与合并

先说压缩。我见过太多项目,JS 和 CSS 文件动辄几百 KB,甚至还有没压缩的版本直接上线。这简直是性能杀手。

压缩的本质很简单:去掉空格、注释、换行,缩短变量名。但效果很显著。一个典型的 React 项目,压缩后体积能减少 60%-70%。

核心原则:生产环境必须使用压缩后的资源文件。开发环境用未压缩版本方便调试,但上线前一定要走构建流程。

合并呢?HTTP/1.1 时代,合并文件能减少请求数,效果立竿见影。但 HTTP/2 普及后,情况变了。HTTP/2 支持多路复用,多个小文件并行下载,反而比一个大文件更快。

所以我的建议是:

  • HTTP/1.1 环境:合并 JS 和 CSS 为少量文件(比如 1-2 个)
  • HTTP/2 环境:按需加载,不强制合并,利用多路复用优势
  • 混合策略:核心框架代码合并,业务代码按路由拆分

我曾经接手过一个老项目,把所有 JS 合并成一个 2MB 的文件。首屏加载要 8 秒。后来改成按路由拆分,首屏只加载 200KB,时间降到 1.2 秒。嗯,差距就是这么大。

4.2 图片优化:WebP 与缩略图

图片往往是页面体积的“大头”。一张未经优化的高清图,可能比整个页面的 JS+CSS 还大。我见过最夸张的案例:一个电商首页,图片占了总资源体积的 85%。

先说 WebP。这个格式由 Google 推出,支持有损和无损压缩。同样画质下,WebP 比 JPEG 小 25%-35%,比 PNG 小 26%。

我的做法:后端图片服务自动判断浏览器是否支持 WebP(通过 Accept 请求头)。支持就返回 WebP,不支持就回退到 JPEG/PNG。前端不用改任何代码。

再说 缩略图。很多开发者喜欢直接加载原图,然后用 CSS 限制显示尺寸。这很浪费。浏览器下载的还是原图,只是显示小了而已。

正确的做法是:

  • 列表页使用 200px-400px 宽的缩略图
  • 详情页使用 800px-1200px 宽的展示图
  • 大图预览才加载原图(2000px+)

我习惯用 srcset 属性来实现响应式图片:

<img src="photo-400.jpg"
     srcset="photo-400.jpg 400w,
             photo-800.jpg 800w,
             photo-1200.jpg 1200w"
     sizes="(max-width: 600px) 400px,
            (max-width: 1200px) 800px,
            1200px"
     alt="示例图片">

这样浏览器会根据屏幕宽度和设备像素比,自动选择最合适的图片。用户不会浪费流量下载超大图。

注意:不要只提供一种尺寸的图片。移动端和桌面端的网络环境差异很大。我曾经见过一个项目,移动端加载 2000px 宽的图片,用户流量直接爆炸。

4.3 布局层级简化

布局层级,说白了就是 DOM 树的深度。浏览器渲染页面时,需要从根节点开始,逐层计算每个元素的位置和大小。层级越深,计算越慢。

我见过最离谱的案例:一个简单的卡片组件,嵌套了 12 层 div。为什么?因为开发者每加一个样式需求,就包一层 div。结果就是:

  • 渲染时间增加 3 倍
  • 内存占用增加 2 倍
  • 滚动卡顿明显

怎么简化?我的经验是:

  1. 能用 CSS 解决的问题,不要加 DOM 元素。比如用 ::before 和 ::after 代替额外的 div
  2. 避免不必要的包裹层。很多框架(比如 React)喜欢用 Fragment,但开发者习惯性加 div
  3. 使用 Flexbox 和 Grid。它们能减少大量用于布局的中间容器
  4. 控制组件嵌套深度。我一般限制在 5 层以内,超过就考虑重构

举个例子,一个典型的卡片组件,优化前可能是这样:

<div class="card">
  <div class="card-wrapper">
    <div class="card-inner">
      <div class="card-header">
        <div class="card-title">标题</div>
      </div>
      <div class="card-body">
        <div class="card-content">内容</div>
      </div>
    </div>
  </div>
</div>

优化后:

<div class="card">
  <h2 class="card-title">标题</h2>
  <p class="card-content">内容</p>
</div>

层级从 6 层降到 3 层,渲染性能提升 40%。

4.4 异步布局加载

有些布局不是首屏必须的。比如弹窗、侧边栏、底部内容。如果这些布局的 CSS 和 JS 都阻塞主线程,首屏渲染就会被拖慢。

异步布局加载的核心思路:首屏只加载关键布局,非关键布局延迟加载。

具体怎么做?我常用的方法:

  • CSS 异步加载:使用 <link rel="preload"> 配合 onload 事件
  • 组件懒加载:使用 React.lazy() 或 动态 import()
  • 条件渲染:等用户交互时再加载对应布局

举个例子,一个弹窗组件:

// 不要这样
import Modal from './Modal';

// 要这样
const Modal = React.lazy(() => import('./Modal'));

// 使用时
{showModal && <Suspense fallback={null}>
  <Modal />
</Suspense>}

这样弹窗的代码只在用户触发显示时才会加载。首屏体积减少,渲染更快。

我的习惯:把页面分成“关键布局”和“非关键布局”。关键布局同步加载,非关键布局异步加载。非关键布局包括:弹窗、侧边栏、页脚、评论区、推荐内容等。

下面这张图展示了资源与布局优化的整体流程:

资源与布局优化流程 资源与布局优化核心流程 第一阶段:资源优化 文件压缩 JS/CSS/HTML 压缩 图片优化 WebP + 响应式缩略图 合并策略 HTTP/1.1 vs HTTP/2 第二阶段:布局优化 层级简化 减少 DOM 嵌套深度 异步加载 非关键布局延迟加载 条件渲染 用户交互时再加载 首屏渲染时间降低 50%-70% 用户感知流畅,体验提升 资源优化和布局优化要一起做,单做一项效果有限

最后,我整理了一个对比表格,方便你快速参考:

优化项 优化前 优化后 效果
文件压缩 JS 500KB(未压缩) JS 150KB(压缩后) 体积减少 70%
图片格式 JPEG 200KB WebP 130KB 体积减少 35%
缩略图 原图 2MB 缩略图 50KB 体积减少 97.5%
布局层级 DOM 深度 12 层 DOM 深度 4 层 渲染时间减少 60%
异步加载 全部同步加载 非关键布局异步 首屏时间减少 40%

嗯,这些优化做下来,页面启动速度会有质的提升。用户不会感觉到“卡”,而是“唰”一下就出来了。这就是我们追求的无感启动。