加载优化
🎯 引言
打开一个网页,用户感知的第一步就是加载速度:图片半天不出来、点进新页面白屏好几秒,用户很可能直接关掉。好消息是,很多加载问题不需要高深的技术,几个常用手段就能明显改善。
学完本篇,你将能够:
- 用原生
loading="lazy"一行属性实现图片懒加载,并理解 IntersectionObserver 手动实现的思路。 - 用动态
import()做路由懒加载,说清楚「打包拆分、访问时才下载」是怎么回事。 - 分清
preload(预加载)和prefetch(预取)这对容易混淆的概念,知道各自的使用场景。 - 理解代码分包的核心思想:首屏用不到的代码,就不让它挡在首屏前面。
🧱 图片懒加载:loading="lazy"
先说一个常见场景:一篇文章里有几十张配图,但用户打开页面时只能看到首屏的一两张。如果浏览器一口气把几十张图全部下载,首屏就要等很久,还浪费了用户可能根本不会往下看的流量。
懒加载的思路是:图片先不加载,等它快要进入可视区域时再加载。以前实现它要写不少 JavaScript,现在浏览器原生支持了,只需要给 img 标签加一个属性:
<!-- 首屏马上能看到的图,正常加载 -->
<img src="banner.png" alt="顶部横幅" />
<!-- 首屏之外的图,加上 loading="lazy" -->
<img src="photo-1.png" alt="配图1" loading="lazy" />
<img src="photo-2.png" alt="配图2" loading="lazy" />
浏览器会自动判断图片和可视区域的距离,快滚动到它时才发起下载。这个方案兼容性好、零成本,是图片懒加载的优先推荐做法。
loading="lazy",否则浏览器反而可能推迟它的加载,拖慢首屏。🧱 图片懒加载:IntersectionObserver 思路
原生属性虽好,但了解背后的手动实现思路仍然值得:面试常问,而且「等元素进入可视区域再做事」这个思路还能用在无限滚动、曝光统计等场景。
核心工具是浏览器提供的 IntersectionObserver(交叉观察器)。你可以把它想象成一个「哨兵」:你让它盯着某个元素,一旦这个元素进入(或离开)可视区域,它就通知你。我们利用这个通知,把图片的真实地址换上:
<!-- 注意:src 先放空或占位图,真实地址存在 data-src 里 -->
<img class="lazy-img" data-src="photo-1.png" alt="配图1" />
// 创建一个哨兵,盯着所有 .lazy-img 元素
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// isIntersecting 为 true,表示元素进入可视区域了
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 把真实地址换上,浏览器开始下载
observer.unobserve(img); // 加载完就不用再盯它了
}
});
});
document.querySelectorAll('.lazy-img').forEach((img) => {
observer.observe(img); // 让哨兵开始盯每一张图
});
整个流程就三步:真实地址先存在 data-src 里 → 哨兵发现图片进入可视区域 → 把地址换到 src 上触发下载。日常开发中用原生 loading="lazy" 就够了,手动实现了解思路即可。
🧱 路由懒加载
懒加载的思路不只适用于图片,也适用于整个页面。一个 Vue 单页应用可能有十几个路由页面,如果打包时把它们全部塞进一个 JS 文件,用户打开首页时就要下载全部代码,首屏自然慢。
路由懒加载的做法是:路由配置里不直接引入组件,而是写一个返回 import() 的函数:
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
// 首页直接引入,首屏就要用
{ path: '/', component: Home },
// 其他页面用动态 import,访问到时才加载
{ path: '/about', component: () => import('./views/About.vue') },
{ path: '/user', component: () => import('./views/User.vue') },
],
});
关键就在 () => import('./views/About.vue') 这个写法上。import() 是动态导入语法,打包工具(Webpack、Vite 都支持)见到它时,会把 About.vue 单独拆成一个文件,而不是合并进主包。运行时的效果就是:用户访问 /about 路由的那一刻,浏览器才去下载 About 页面的代码。
这样首页只需下载「公共代码 + 首页代码」,体积小了,首屏自然就快了。至于首屏必经的首页,保持直接引入即可。
⚡ 预加载 preload 与预取 prefetch
懒加载是「推迟不急着要的」,反过来还有一对「提前准备马上要的」的手段:preload(预加载) 和 prefetch(预取)。两者都写在 <link rel> 里,长得像、名字像,是初学者非常容易混淆的点,我们对比着讲清楚。
preload:当前页面马上就要用的资源,高优先级提前下载。
比如页面用一个自定义字体渲染大标题,浏览器要等到 CSS 解析完才发现这个字体,标题会先显示成默认字体再「跳变」。用 preload 可以让浏览器尽早开始下载:
<head>
<!-- 当前页面渲染马上要用,提前高优先级下载 -->
<link rel="preload" href="/fonts/title.woff2" as="font" type="font/woff2" crossorigin />
</head>
注意 as 属性必须写,它告诉浏览器资源类型(font、script、style、image 等),浏览器据此安排优先级并复用下载结果。
prefetch:下一个页面可能要用的资源,低优先级空闲时下载。
比如用户在产品列表页,大概率下一步会点进详情页。我们可以让浏览器在当前页面加载完、网络空闲时,悄悄把详情页的代码先下载好,用户点击时秒开:
<head>
<!-- 下一个页面可能要用,利用空闲时间低优先级下载 -->
<link rel="prefetch" href="/js/detail-page.js" />
</head>
一句话记住区别:
| 对比项 | preload | prefetch |
|---|---|---|
| 服务对象 | 当前页面的资源 | 下一个页面的资源 |
| 优先级 | 高,马上开始下载 | 低,当前页面加载完空闲才下载 |
| 典型场景 | 首屏字体、关键 CSS/JS | 下一步可能访问的页面代码 |
你也可以在 Chrome DevTools 的 Network 面板里观察两类资源的加载时机和优先级。
🧱 代码分包
最后把视角拉高一点,看看这些手段背后的共同思想:代码分包。
打包工具的默认行为是把所有代码合并成少量文件。项目小没问题,项目一大,主包就会膨胀,首屏要下载和解析的东西越来越多。分包就是把这坨代码按使用时机拆开:首屏需要的留在主包里,首屏用不到的拆出去,用到时再下载。
其实我们这篇已经见过两种分包了:路由懒加载是「按路由拆」,import() 出现的地方就是拆分的边界;prefetch 预取的也常是拆出去的异步包。除此之外还有常见的「按依赖拆」,比如把 vue、vue-router 这类很少变动的第三方库拆成单独的文件,业务代码更新时它们还能继续命中缓存。
manualChunks、Webpack 的 splitChunks)属于构建工具的范畴,详见 Vite 课程 和 Webpack 课程,这里不重复展开,理解「按需拆分」的思想即可。判断要不要拆,标准很朴素:这个模块首屏需要吗? 不需要(比如管理后台的图表页、弹窗里的复杂编辑器),就拆出去按需加载。
🧾 小节总结
- 图片懒加载优先用原生
loading="lazy",一行属性搞定;首屏立即可见的图不要加。 - IntersectionObserver 像「哨兵」,元素进入可视区域时通知你,手动懒加载就是此时把
data-src换到src。 - 路由懒加载用
() => import('./About.vue'),打包时拆成单独文件,访问路由时才下载。 - preload 管当前页面:高优先级、马上下载,给首屏关键资源用;prefetch 管下一个页面:低优先级、空闲时下载。
- 代码分包的核心思想:首屏用不到的代码拆出去按需加载,具体配置交给构建工具。
❓ 知识问答
Q1:loading="lazy" 所有浏览器都支持吗?
A:主流现代浏览器(Chrome、Firefox、Safari、Edge 的较新版本)都已支持。对于不支持的旧浏览器,这个属性会被忽略,图片退化为立即加载,功能不会出错,只是没有懒加载效果。
Q2:路由懒加载后,组件里的代码都会在访问时才下载吗?
A:是的,该组件自身以及它独有依赖的模块都会打进那个单独的文件里。但如果某个模块同时被主包和懒加载路由引用,打包工具通常会把它提到公共部分,避免重复下载。
Q3:preload 和 prefetch 可以同时用吗?
A:可以,两者服务的资源不同:当前页面关键资源用 preload,下一页资源用 prefetch,互不冲突。但要注意总量,预加载太多资源会占用带宽,反而拖慢真正重要的加载。
Q4:为什么 preload 必须写 as 属性?
A:as 告诉浏览器资源类型,浏览器才能设置正确的下载优先级,并在后续真正使用这个资源时复用已下载的内容。漏写 as 会导致资源可能被下载两次,preload 就白做了。
Q5:路由懒加载和 prefetch 是什么关系?
A:两者是配合关系。懒加载决定「拆出去、用到时才下载」,prefetch 决定「空闲时提前下载好」。不少框架(如 Nuxt)会自动为路由分包生成 prefetch 提示,让懒加载的页面也能秒开。
🧪 小练习
练习一:给一个路由配置补上懒加载。下面的 User.vue 目前是直接引入的,把它改成访问时才加载的写法:
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';
import User from './views/User.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
// 请在这里编写代码:把 User 路由改成懒加载写法
{ path: '/user', component: User },
],
});
练习二:下面这个页面,大标题用了自定义字体 /fonts/title.woff2,且用户大概率会接着访问「关于页」(代码在 /js/about-page.js)。在 <head> 中补上合适的 <link> 提示,注意选对 rel 和优先级:
<head>
<!-- 请在这里编写代码:预加载首屏字体 -->
<!-- 请在这里编写代码:预取关于页代码 -->
</head>
完成后想想:两个资源的 rel 为什么不一样?如果把它们对调,会发生什么?
🎉 恭喜你已经掌握加载优化技能啦!下一篇我们进入渲染优化,看看页面加载完之后,怎么让它跑得更流畅。
