前端页面性能优化(三)

前端页面性能优化之加载顺序与首屏体验

前两文我们分别从图片静态资源与缓存的角度,讨论了如何让页面“更轻、更省流量”。但真正决定用户主观体验的,还有一个更关键的问题:

用户到底在什么时候,能看到“有内容的页面”和“可以操作的页面”?

本文章将重点围绕:

  • 浏览器核心性能指标(FCP / LCP / TTI / CLS)
  • 关键渲染路径与阻塞因素(CSS / JS / 字体)
  • 首屏加载顺序设计(Critical CSS、骨架屏、接口策略)
  • 懒加载与按需加载(图片、组件、路由)

一、首屏相关的核心指标

首先要知道,我们到底要“优化的是什么”。常见的几个核心指标:

  • FCP(First Contentful Paint):首次内容绘制,用户第一次看到任何文本、图片、非白屏内容的时间。
  • LCP(Largest Contentful Paint):最大内容绘制,视口中最大的内容块(通常是大图、主标题、主卡片)绘制完成的时间。
  • TTI(Time to Interactive):可交互时间,页面已经可以稳定响应用户输入的时间。
  • CLS(Cumulative Layout Shift):累计布局偏移,代表页面布局在加载过程中“抖动”的程度。

简单理解:

  • 想要“白屏时间短” → 看 FCP。
  • 想要“主要内容早点出来” → 看 LCP。
  • 想要“点了就有反应” → 看 TTI。
  • 想要“元素不要乱跳” → 看 CLS。

首屏优化,就是围绕这几个指标做“有针对性”的调整。


二、关键渲染路径与阻塞资源

要优化加载顺序,首先要理解浏览器是如何把 HTML 变成“可见页面”的。

一个极简版流程:

  1. 解析 HTML → 构建 DOM 树。
  2. 解析 CSS → 构建 CSSOM。
  3. 合并 DOM + CSSOM → 生成 Render Tree。
  4. 布局(Layout) → 绘制(Paint)→ 合成(Composite)。

在这个过程中,有几类资源会对“首屏渲染”产生关键影响:

  • CSS:默认是渲染阻塞的。
  • 同步 JS:可能阻塞 HTML 解析与渲染。
  • 字体:影响文字绘制策略,可能导致空白或闪烁。
  • 大图与首屏关键图片:会影响 LCP。

理解这点后,我们的目标可以具体化为:

首屏必须要的资源尽快、优先、顺序合理地加载;让非首屏必须的资源尽量延后或懒加载。


三、CSS 加载优化:尽量“快且少”

1. Critical CSS:内联关键样式

对于首屏结构所依赖的关键 CSS,可以选择直接内联进 HTML,避免首屏等待外部 CSS 请求完成。

示例(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
<style>
/* 只放首屏必要样式 */
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.hero {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<link rel="stylesheet" href="/static/index.41d2e8e1.css" />
</head>

实践要点:

  • 不要把全部 CSS都内联,这会导致 HTML 体积暴涨,影响后续缓存策略。
  • 建议配合构建工具提取首屏关键 CSS,或手动控制“首页 Hero 区、导航条等骨干结构”的样式。

2. 拆分 CSS:路由级 / 功能级

传统做法是把所有页面的 CSS 打在一个包里,结果是:

  • 首屏页面只用到 20% 的样式。
  • 用户却要为 100% 的样式等待与下载。

更推荐的做法是:

  • 基础样式(reset、主题色、通用组件)单独一个包。
  • 各路由或大模块的 CSS 分包。

配合路由懒加载,可以实现:

  • 访问 /home 只下载 home.css
  • 访问 /dashboard 时再下载 dashboard.css

3. 避免使用 @import 链式引入

在 CSS 中使用:

1
2
@import url("reset.css");
@import url("layout.css");

会增加额外的请求等待和解析开销。相比之下,直接在 HTML 中用多个 <link> 更好:

1
2
<link rel="stylesheet" href="/css/reset.css" />
<link rel="stylesheet" href="/css/layout.css" />

更进一步的优化,就是在构建阶段把多个 CSS 合并 / 按需拆分,而不是在线上用 @import


四、JavaScript 加载与执行优化:别阻塞渲染

JS 是现代 Web 的灵魂,但也是性能问题的重灾区。

1. defer / async:脚本加载方式选择

当我们在 <head> 中引入 JS 时,如果不做任何处理:

1
<script src="/static/app.js"></script>

浏览器会:

  1. 停止解析 HTML。
  2. 下载并执行脚本。
  3. 执行完再继续往下解析。

这就有可能严重阻塞首屏渲染。

更推荐的方式是:

  • 非必须在第一时间执行的脚本,加上 deferasync
1
2
3
4
5
6
<!-- 推荐:defer,按顺序执行,且不会阻塞 HTML 解析 -->
<script src="/static/vendor.js" defer></script>
<script src="/static/app.js" defer></script>

<!-- async:适合互不依赖的第三方脚本,如埋点 / 广告等 -->
<script src="https://example.com/analytics.js" async></script>

区别简记:

  • defer:下载异步,按顺序执行,在 DOM 解析完成后执行,不阻塞渲染。
  • async:下载异步,谁先下载完谁先执行,可能打乱顺序,适合不依赖 DOM、也不被其他脚本依赖的场景。

2. 代码拆分与路由懒加载

对 SPA 来说,常见问题是:

  • 打包产物一个 app.js,体积非常大。
  • 用户打开首页时,其实只用到了 30% 的代码。

改进方式:

  • 按路由拆分:每个路由一个或多个 chunk。
  • 按功能模块拆分:图表库、富文本编辑器、低频使用组件等单独拆出。

示例(以 Vue Router 为例,简化说明):

1
2
3
4
5
6
7
8
9
10
const routes = [
{
path: "/",
component: () => import("./pages/Home.vue"),
},
{
path: "/about",
component: () => import("./pages/About.vue"),
},
];

这样:

  • 首次打开 /,只下载 Home 页相关代码。
  • 当用户访问 /about 时,再按需加载 About 页脚本。

3. 减少首屏同步 JS 逻辑

很多时候,我们在入口脚本中做了“太多事情”:

  • 页面初始化就拉取多组接口。
  • 马上执行各种事件绑定与初始化逻辑。
  • 甚至在首屏阶段就加载大量第三方 SDK。

优化思路:

  • 把首屏**“必要的交互”“必要的数据”**优先处理。
  • 把非必要逻辑延后到 requestIdleCallback、首屏渲染后、用户首次交互之后等时机。

举个非常简单的示例:

1
2
3
4
5
6
7
8
// 首屏必须:拉取首页主列表数据
fetchHomeList();

// 非必须:埋点初始化、次要区块数据
window.addEventListener("load", () => {
initAnalytics();
fetchSecondaryData();
});

五、骨架屏与占位:减少“主观白屏时间”

有时候,接口响应的客观时间可能很难再压缩,但我们可以优化用户的“主观等待感受”。

1. 骨架屏(Skeleton Screen)

骨架屏的核心理念是:

与其让用户看到一片白,不如先展示一个“灰色框架”,暗示这里有内容正在加载。

例如:

  • 对列表:显示若干条灰色条形块。
  • 对详情页:显示标题条、图片占位、若干段落占位。

在实现上,可以:

  • 使用纯 CSS + div 实现骨架结构。
  • 或使用 UI 组件库内置的 Skeleton 组件(如 Ant Design、Element Plus 等)。

2. 图片占位与渐进加载

对大图、Banner 图等,可以:

  • 先渲染一个低清晰度的缩略图(LQIP:Low Quality Image Placeholder)。
  • 当高清图加载完成再平滑替换。

这样可以显著改善 LCP 体验,避免空白空间长时间占位。


六、懒加载:把“不急用”的东西往后放

懒加载的本质是:

只在“需要的时候”才加载对应资源

1. 图片懒加载

现代浏览器已经原生支持 loading="lazy" 属性:

1
<img src="/images/article-1.webp" alt="文章封面" loading="lazy" />

对于首屏外的图片,我们可以:

  • 手动添加 loading="lazy"
  • 或通过框架 / 组件库提供的 LazyImage 组件。

对更精细的控制,可以使用 IntersectionObserver

1
2
3
4
5
6
7
8
9
10
11
12
13
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});

document.querySelectorAll("img[data-src]").forEach((img) => {
observer.observe(img);
});

2. 组件懒加载 / 路由懒加载

这一部分与上文的 JS 分包关系紧密:

  • 路由级别的懒加载:用户访问某路由时才加载对应组件代码。
  • 组件级别的懒加载:例如富文本编辑器、图表、地图等大体积组件只在真的需要时才加载。

以 React 为例(简化说明):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const HeavyEditor = React.lazy(() => import("./HeavyEditor"));

function Page() {
const [showEditor, setShowEditor] = useState(false);

return (
<div>
<button onClick={() => setShowEditor(true)}>打开编辑器</button>
{showEditor && (
<React.Suspense fallback={<div>加载编辑器中...</div>}>
<HeavyEditor />
</React.Suspense>
)}
</div>
);
}

这样,首屏并不会加载 HeavyEditor 的代码和依赖,只有用户真正点开时才会触发加载。


七、接口策略与首屏数据获取

首屏渲染不仅依赖静态资源,还依赖 API 数据。常见的几种策略:

1. CSR:纯前端渲染 + 接口拉取

流程大致为:

  1. 浏览器下载 HTML(简陋占位)。
  2. 下载 JS / CSS。
  3. JS 执行后发起接口请求。
  4. 数据返回后再渲染内容。

问题:

  • 网络链路长,首屏可见内容时间较晚。
  • 对弱网、移动端不友好。

2. SSR:服务端渲染

SSR 的流程是:

  1. 服务端直接将“带数据的 HTML”渲染好返回。
  2. 浏览器几乎可以在 HTML 到达后就完成首屏内容展示。
  3. 客户端 JS 再“水合”(Hydration)绑定事件。

优势:

  • FCP / LCP 通常都有明显改善。
  • 对 SEO 更友好。

成本:

  • 服务端压力更高。
  • 开发模式更复杂,需要考虑同构、数据获取时机等。

3. SSG / 预渲染(Pre-render)

对于内容相对稳定的页面(如博客、文档、营销页),可以:

  • 在构建阶段就生成好 HTML。
  • 部署时直接以静态文件形式托管。

这样既保留 SSR 的首屏体验优势,又减轻了运行时服务器压力。

4. 混合策略与渐进增强

在实际项目中,经常会采用混合策略:

  • 首屏关键页面(首页、主要业务入口)采用 SSR / SSG。
  • 其他路由使用 CSR + 懒加载。
  • 对复杂交互和数据实时性要求高的页面,再按需做优化。

八、避免布局抖动:优化 CLS

CLS(累计布局偏移)高,会让用户感觉页面“在脚底下乱跳”。

常见原因:

  • 图片未设置宽高,加载后突然把后续内容挤下去。
  • 广告、推荐位动态插入 DOM,导致布局重新排布。
  • 字体加载导致文字宽度变化。

优化建议:

  • 为图片/视频提前设置宽高或使用占位容器。
1
<img src="/images/banner.webp" alt="banner" width="1200" height="400" />
  • 对于异步内容(广告、推荐位),提前预留容器高度。
  • 通过前文提到的字体优化(font-display / 子集化)减少字体引起的布局抖动。

九、首屏优化实践清单

最后给出一个可以直接作为“Checklist”的实践清单,你可以在项目中对照使用:

  • HTML / 结构

    • 首屏骨架结构是否简单清晰?
    • 是否有必要的骨架屏或占位?
  • CSS

    • 是否为首屏提取了 Critical CSS?
    • 是否避免了层层 @import
    • CSS 是否按路由 / 模块拆分?
  • JavaScript

    • 是否合理使用 defer / async
    • 是否对路由和大体积组件做了懒加载?
    • 首屏入口代码中是否存在可延后的逻辑?
  • 图片与资源

    • 首屏大图是否做了压缩与格式优化(参考本系列(一))?
    • 是否有首屏外图片的懒加载?
    • 是否避免了大量首屏不必要的第三方脚本?
  • 数据与渲染方式

    • 是否评估过使用 SSR / SSG 的收益?
    • 首屏接口是否进行了合并、裁剪或缓存?
  • 布局稳定性

    • 图片 / 广告位是否预留空间?
    • 字体加载策略是否会导致布局大幅变化?

十、总结

本篇从 指标 → 关键路径 → 加载顺序 → 懒加载 / 骨架屏 → 渲染策略 的顺序,系统梳理了首屏体验优化的思路:

  • 明确优化目标:FCP / LCP / TTI / CLS。
  • 控制阻塞资源:CSS 与同步 JS 的加载与执行。
  • 把首屏必须的资源前置,把非首屏资源懒加载。
  • 用骨架屏与占位减少“主观白屏时间”与布局抖动。
  • 结合 SSR / SSG 等技术手段,从根本上缩短首屏渲染链路。

有了这套体系,你就不再是“零散地优化某个点”,而是可以从整体架构的角度,设计出一套适合自己项目的首屏性能优化方案。


前端页面性能优化(三)
https://sunjc.vip/2025/09/12/前端页面性能优化(三)/
作者
Sunjc
发布于
2025年9月12日
许可协议