性能优化简介

加载优化

掌握图片懒加载、路由懒加载、预加载与代码分包等常用加载优化手段。

🎯 引言

打开一个网页,用户感知的第一步就是加载速度:图片半天不出来、点进新页面白屏好几秒,用户很可能直接关掉。好消息是,很多加载问题不需要高深的技术,几个常用手段就能明显改善。

学完本篇,你将能够:

  • 用原生 loading="lazy" 一行属性实现图片懒加载,并理解 IntersectionObserver 手动实现的思路。
  • 用动态 import() 做路由懒加载,说清楚「打包拆分、访问时才下载」是怎么回事。
  • 分清 preload(预加载)和 prefetch(预取)这对容易混淆的概念,知道各自的使用场景。
  • 理解代码分包的核心思想:首屏用不到的代码,就不让它挡在首屏前面。

🧱 图片懒加载:loading="lazy"

先说一个常见场景:一篇文章里有几十张配图,但用户打开页面时只能看到首屏的一两张。如果浏览器一口气把几十张图全部下载,首屏就要等很久,还浪费了用户可能根本不会往下看的流量。

懒加载的思路是:图片先不加载,等它快要进入可视区域时再加载。以前实现它要写不少 JavaScript,现在浏览器原生支持了,只需要给 img 标签加一个属性:

index.html
<!-- 首屏马上能看到的图,正常加载 -->
<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(交叉观察器)。你可以把它想象成一个「哨兵」:你让它盯着某个元素,一旦这个元素进入(或离开)可视区域,它就通知你。我们利用这个通知,把图片的真实地址换上:

index.html
<!-- 注意:src 先放空或占位图,真实地址存在 data-src 里 -->
<img class="lazy-img" data-src="photo-1.png" alt="配图1" />
lazy.js
// 创建一个哨兵,盯着所有 .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() 的函数:

router.js
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 页面的代码

这样首页只需下载「公共代码 + 首页代码」,体积小了,首屏自然就快了。至于首屏必经的首页,保持直接引入即可。

vue-router 的安装与基础配置在 Node.js v22 + vue-router 4.x 下验证,完整用法见 Vue3 使用课程,这里只关注懒加载写法。

⚡ 预加载 preload 与预取 prefetch

懒加载是「推迟不急着要的」,反过来还有一对「提前准备马上要的」的手段:preload(预加载)prefetch(预取)。两者都写在 <link rel> 里,长得像、名字像,是初学者非常容易混淆的点,我们对比着讲清楚。

preload:当前页面马上就要用的资源,高优先级提前下载。

比如页面用一个自定义字体渲染大标题,浏览器要等到 CSS 解析完才发现这个字体,标题会先显示成默认字体再「跳变」。用 preload 可以让浏览器尽早开始下载:

index.html
<head>
    <!-- 当前页面渲染马上要用,提前高优先级下载 -->
    <link rel="preload" href="/fonts/title.woff2" as="font" type="font/woff2" crossorigin />
</head>

注意 as 属性必须写,它告诉浏览器资源类型(fontscriptstyleimage 等),浏览器据此安排优先级并复用下载结果。

prefetch:下一个页面可能要用的资源,低优先级空闲时下载。

比如用户在产品列表页,大概率下一步会点进详情页。我们可以让浏览器在当前页面加载完、网络空闲时,悄悄把详情页的代码先下载好,用户点击时秒开:

index.html
<head>
    <!-- 下一个页面可能要用,利用空闲时间低优先级下载 -->
    <link rel="prefetch" href="/js/detail-page.js" />
</head>

一句话记住区别:

对比项preloadprefetch
服务对象当前页面的资源下一个页面的资源
优先级高,马上开始下载低,当前页面加载完空闲才下载
典型场景首屏字体、关键 CSS/JS下一步可能访问的页面代码
preload 是「声明这个资源很重要」,用错了反而有害:把不关键的资源标成高优先级,会挤占真正关键资源的带宽,只给确定马上要用的资源加。

你也可以在 Chrome DevTools 的 Network 面板里观察两类资源的加载时机和优先级。

Chrome DevTools 的界面会不定期更新,面板名称和入口位置可能与你看到的略有不同。以上观察思路不变,操作时以实际界面为准。

🧱 代码分包

最后把视角拉高一点,看看这些手段背后的共同思想:代码分包

打包工具的默认行为是把所有代码合并成少量文件。项目小没问题,项目一大,主包就会膨胀,首屏要下载和解析的东西越来越多。分包就是把这坨代码按使用时机拆开:首屏需要的留在主包里,首屏用不到的拆出去,用到时再下载。

其实我们这篇已经见过两种分包了:路由懒加载是「按路由拆」,import() 出现的地方就是拆分的边界;prefetch 预取的也常是拆出去的异步包。除此之外还有常见的「按依赖拆」,比如把 vuevue-router 这类很少变动的第三方库拆成单独的文件,业务代码更新时它们还能继续命中缓存。

具体的分包配置(如 Vite 的 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 为什么不一样?如果把它们对调,会发生什么?


🎉 恭喜你已经掌握加载优化技能啦!下一篇我们进入渲染优化,看看页面加载完之后,怎么让它跑得更流畅。