图片往往是页面上体积最大的静态资源,它们在何时加载、如何加载,直接影响着首屏速度和用户流量消耗。同样,对于一些后续页面必然用到的关键资源,提前加载又能让交互体验更流畅。两者并不矛盾,分别解决不同场景下的性能问题。
图片懒加载:只加载看得见的图片
图片懒加载的核心思路是:首屏只加载视口内的图片,其余图片暂时用占位符替代,等到用户滚动到它们附近时再加载。这样能大幅减少首屏的网络请求数量和下载体积,让首屏更快可交互,同时为按流量计费的移动端用户省下不必要的下载。
Vue 中的落地方式有三种主流选择:
1. 原生方案:浏览器级的 loading="lazy"
这是最简单、零依赖的懒加载方式。只需要在 <img> 标签上添加 loading="lazy" 属性,浏览器会自动判断元素是否接近视口并触发加载。
<img src="poster.jpg" loading="lazy" alt="电影海报" />
优点是完全不需要写任何 JavaScript,兼容性在现代浏览器中已经很好(Chrome 77+、Edge 79+、Firefox 75+)。缺点是你无法控制加载阈值、占位图样式、加载失败处理等细节。对于简单的列表页展示,这个方案已经足够实用。
2. 指令封装方案:自定义 v-lazy 指令
当需要更灵活的控制(比如自定义占位图、加载动画、错误回退图)时,可以基于 Intersection Observer API 封装一个自定义指令。
// directives/lazy.js
import defaultPlaceholder from '@/assets/placeholder.png'
export default {
mounted(el, binding) {
// 设置初始占位图
el.src = defaultPlaceholder
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// 进入视口,加载真实图片
const realSrc = binding.value
const img = new Image()
img.src = realSrc
img.onload = () => {
el.src = realSrc
el.classList.add('loaded')
}
img.onerror = () => {
// 可设置错误回退图
el.src = defaultPlaceholder
}
observer.unobserve(el)
}
},
{ rootMargin: '50px' } // 提前 50px 开始加载,提升体验
)
observer.observe(el)
}
}
在组件中使用:
<img v-lazy="item.coverUrl" alt="文章封面" />
这种封装可以全局注册,之后任何地方只需一行指令即可。相比于第三方库,自己写的指令体积为零,逻辑完全可控。
3. 第三方库:vue-lazyload 等
如果项目对懒加载的需求非常复杂(比如支持背景图懒加载、视频懒加载、自定义加载组件),可以使用社区库如 vue-lazyload。但需要注意,Vue 3 生态下部分老库可能更新不及时,选用时要确认兼容性。多数项目用原生属性或自定义指令就足够了。
资源预加载:偷偷把资源提前备好
资源预加载的思路与懒加载正好相反:某些资源虽然当前页面上没用到,但用户极大概率马上就会需要(比如翻页的下一张图片、弹窗内的大图),提前加载它们,等到真正展示时就能瞬间呈现。
预加载的实施方式:
1. <link> 标签预加载
在 HTML 头部通过 <link rel="preload"> 或 <link rel="prefetch"> 告诉浏览器提前获取资源。
preload:高优先级,用于当前页面马上要用到的资源(比如初始不可见但很快会渲染的字体文件、首屏关键图片)。prefetch:低优先级,用于未来可能导航到的下一个页面的资源(比如下一页的数据、下一页的图片)。
<!-- 提前加载首屏轮播的下一张图片 -->
<link rel="preload" as="image" href="next-slide.jpg" />
<!-- 提前加载用户可能点击进入的详情页数据 -->
<link rel="prefetch" href="detail-page.js" />
在 Vue 中,你可以在 public/index.html 中写死关键资源的 preload,或者利用 vue-meta、unhead 等方案动态生成。
2. JavaScript 动态预加载
用 Image 对象或 fetch 在空闲时间偷偷下载资源。
// 在进入某个路由后,预加载下一张可能需要的大图
const preloadImages = (urls) => {
urls.forEach(url => {
const img = new Image()
img.src = url
})
}
// 比如商品列表页,鼠标悬停在某个商品上时,预加载它的详情图
function onHoverItem(item) {
preloadImages([item.detailImage])
}
这种动态预加载可以结合 requestIdleCallback 或 setTimeout,在浏览器空闲时执行,避免与主业务争抢网络和计算资源。
3. 路由懒加载 + 魔法注释预加载
当使用 Vue Router 进行路由级别的代码分割时(() => import(...)),可以通过 Webpack 的魔法注释 / webpackPrefetch: true / 让打包工具自动在空闲时间预加载该路由组件。
const DetailPage = () => import(
/* webpackPrefetch: true */
'@/views/DetailPage.vue'
)
这样用户访问列表页时,DetailPage 的 JS 和 CSS 就会在后台悄悄下载,点击进入详情页时几乎秒开。这个技巧对提升后面页面的加载速度非常有效,而且配置成本极低。
结合使用的场景决策
- 首屏不可见、且体积大的图片 → 懒加载(节省首屏带宽)
- 用户极大概率点击的“下一页”或“详情”的关键图片/脚本 → 预加载(提升后续交互速度)
- 弹窗内的大图 → 可以在弹窗触发前预加载,也可以在弹窗打开时懒加载边框外的图片,视弹窗触发频率和图片大小而定
在实际项目中,通常会同时使用两种策略:对整个列表的封面图做懒加载,同时对当前浏览的商品下一张细节图做预加载。这样既保证了首屏速度,又保证了浏览体验的流畅。
最后提醒一点:预加载要适度。预加载太多资源会抢占首屏请求的带宽,反而拖慢首屏。优先预加载用户最可能需要的下一张/下一页资源,并尽量在浏览器空闲时进行。