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 倍
- 滚动卡顿明显
怎么简化?我的经验是:
- 能用 CSS 解决的问题,不要加 DOM 元素。比如用
::before和::after代替额外的 div - 避免不必要的包裹层。很多框架(比如 React)喜欢用 Fragment,但开发者习惯性加 div
- 使用 Flexbox 和 Grid。它们能减少大量用于布局的中间容器
- 控制组件嵌套深度。我一般限制在 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 500KB(未压缩) | JS 150KB(压缩后) | 体积减少 70% |
| 图片格式 | JPEG 200KB | WebP 130KB | 体积减少 35% |
| 缩略图 | 原图 2MB | 缩略图 50KB | 体积减少 97.5% |
| 布局层级 | DOM 深度 12 层 | DOM 深度 4 层 | 渲染时间减少 60% |
| 异步加载 | 全部同步加载 | 非关键布局异步 | 首屏时间减少 40% |
嗯,这些优化做下来,页面启动速度会有质的提升。用户不会感觉到“卡”,而是“唰”一下就出来了。这就是我们追求的无感启动。