资源加载优化
刀刀
4/7/2025
0 字
0 分钟
首屏速度是用户体验最关键的一环,资源加载速度是影响性最大的因素。资源的加载速度 = 资源大小 + 网速。资源大小影响的方面有:
- 压缩。压缩可以影响资源大小,但是脚手架和工具基本已经做好了,编程上没法再做更多
- 一部分代码分割出来做异步加载,需要使用的时候再加载出来。这是核心思想
- 代码尽量精简
异步加载
prefetch 加载
假设 Page1
和 Page2
是同步加载的,Page3
是异步加载,则会有以下的流程:
无论是同步还是异步都会先创建一个 link
标签去请求加载,只不过异步的 Page3
会标记 prefetch
。没有该标记则说明是需要优先加载的,同步加载的资源都加载完毕后再去加载异步加载的资源。
通过项目来观察一下 prefetch
加载的过程。无论是组件还是路由,异步加载一般都是使用 import
方法来引入使用,代码如下:
components: {
HelloWord: import("../about/xx.vue");
}
{
path: 'about',
component: () => import('@/views/about')
}
排查方式
前往开发者 network
修改网络为高速 3G,刷新页面可以发现,全部的资源都在请求,只不过 about
资源状态处于 padding
等待状态,直到同步资源加载完毕才加载。
查看源代码,其引入代码有一个 typ="prefetch"
的标识。
script 加载
假设 Page1
和 Page2
是同步加载的,Page3
是异步加载,则会有以下的流程:
一开始只请求同步加载的资源,异步状态资源它不会去加载,等到用户要跳转到 Page3
它才会去加载。
prefetch
加载了,如果想要关闭它,需要前往 vue.config.js
文件修改,关闭后默认采用的 script
加载方式。代码示例如下:module.exports = {
chainWebpack: (config) => {
config.plugins.delete("prefetch");
},
};
如果在关闭 prefetch
的情况下想要部分路由使用 prefetch
加载,可以为其添加注释 webpackPrefetch
,后可以添加 true
(没有 false
这个选项)。或者填入数字,表示优先级。数字越大优先级越高,会优先加载(true
视为 0)。
{
path: 'about',
component: () => import(/* webpackPrefetch: 10 */ '@/views/about')
},
{
path: 'help',
component: () => import(/* webpackPrefetch: 100 */ '@/views/help') // 优先级最高
}
🔔 提示
这个是 webpack
提供的功能,与 vue
无关,如果你是使用 webpack
创建的 react
项目也需要这么配置。
总结
script
- 做到了真正的按需引入,用到的时候再加载,不用永不加载,充分减小带宽
- 最大的问题在于,切换需要等待,体验感不是很流畅
prefetch
- 充分利用使用者不占用带宽的浏览时间,切换到异步加载的页面是可能已经加载好了,用户体验好
- 一些本次行为不会打开的页面也会加载,一定程度上浪费带宽
优化经验
- 使用按需引入(函数式)的版本
- 在组件
mounted
阶段再引入库,或者用到这个功能时再引入 - 利用
prefetch
调控加载顺序
下面依次查看如何操作实现。
使用按需引入(函数式)的版本
如 xlsx
第三方库,有很多方法,我们只用了其中几个方法,但是全部引入他会都打包,项目体积会变大。
import xlsx from "xlsx";
因此可以使用按需引入的方法。方法为:
- 如果是旧版本卸载依赖重新安装最新版本,支持按需引入。(老版本的库可能不支持该方法)
- 采用解构的方式引入我们需要的方法即可
import { read, utils } from "xlsx";
在组件 mounted 阶段再引入库,或者用到这个功能时再引入
有部分功能与页面展示没太大关系,可能功能也不是第一时间使用,因此可以写在函数内,在 mounted
阶段引入。例如 jquery
库,我们只在某个方法中使用到它。因此可以写在函数内,在组件的 mounted
阶段引入。
如果是通过 import xx from 'xx'
引入的第三方库,说明它们内部是通过 export default
全局导出,因此获取到值后通过 .default
获取值。例如jquery源码是通过 export default
导出,因此 res.default
才是 jquery
方法。import
异步引入返回一个 Promise
,可以使用 .then
获取回调参数。
如果是通过 import {xx} from 'xx'
引入的第三方库,说明它们内部是通过 export
按需导出,因此直接获取值即可。前面的 xlsx
也可以使用该方法异步引入,示例代码如下:
<script>
let $; // 在外层定义全部变量赋值,方便在函数内使用
export default {
mounted() {
this.changeTest();
},
methods: {
changeTest() {
import(/* webpackPrefetch: 1000 */ "jquery").then((res) => {
$ = res.default;
let a = $(".test");
a.html("hello");
});
},
},
};
</script>
<script>
let utils;
let read;
export default {
mounted() {
this.changeXlsx();
},
methods: {
changeXlsx() {
import(/* webpackPrefetch: 10 */ "xlsx").then((res) => {
// 这里是按需导出,因此不需要default
read = res.read;
utils = res.utils;
});
},
},
};
</script>
题外话
这种方式虽然减少了好几秒的首屏加载时间,但是代价是用户如果一进来就想要使用对应功能则可能会需要等待一会,有利有弊
图片
骨架屏与占位图
骨架屏
主要作用于在内容加载期间显示的一种空状态预览,通常用灰色块与线条模拟布局。
效果图如下所示:
实现原理主要有以下几种:
- 静态HTML/CSS:最简单的骨架屏可以直接通过静态的HTML结构和CSS样式来实现,设计成与目标加载内容大致相似的布局
- 动态生成:对于更复杂或需要根据数据动态变化的布局,可以使用JavaScript动态生成骨架屏。例如,根据数据模型预估内容结构,用相应的占位符元素填充
- 组件库/工具:一些现代前端框架和组件库提供了内置的骨架屏组件或插件
占位图
占位图是一张 .svg
格式的图片,这类图片尺寸更小,视觉效果更丰富,适用于图形简单的图片,在图片资源加载完毕前可用于占位。
sizes 属性
含义
在响应式网页设计中,使用 <img>
标签的 sizes
属性是一种优化图片加载、提升页面性能的关键技术。
它允许开发者为不同的屏幕尺寸指定最合适的图片资源,确保用户在任何设备上都能获得最佳的视觉体验,同时避免不必要的数据传输。 sizes
属性用于指定图片在不同布局条件下的显示宽度,与 srcset
属性配合使用。
使用方法
- 定义
srcset
:在img
标签中使用srcset
属性,列出不同分辨率的图片资源及其对应的宽度描述符 - 设置
sizes
:通过sizes
属性指定不同视口宽度下图片应占的最大宽度。可以使用媒体条件(如min-width
或max-width
)来定义这些规则 - 指定默认
src
:为了向后兼容不支持srcset
和sizes
属性的浏览器,还需要使用src
属性指定一个默认的图片资源
示例代码
假设有一个网站布局,在屏幕宽度小于 600px 时,图片应占满整个屏幕宽度;在屏幕宽度介于 600px 到 900px 之间时,图片应占屏幕宽度的一半;在屏幕宽度超过 900px 时,图片宽度固定为 450px。
相应的 img
标签代码如下:
<img src="./default.jpg" srcset:"small.jpg 500w, medium.jpg 1000w, large.jpg
1500w" sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 450px"
alt="示例图片">
工作原理
当浏览器解析到 img
标签时,它会查看设备的屏幕宽度,并与 sizes
属性中定义的条件进行匹配。
根据匹配结果,浏览器会从 srcset
中选择一个最接近所需宽度的图片资源加载。如果有多个候选,浏览器会选择最接近且稍微大一点的图片,以避免加载过小而影响图片质量的资源。
如果浏览器不支持 srcset
和 sizes
,则会回退到 src
属性指定的图片资源。
优势
- 性能优化:通过加载与显示尺寸最匹配的图片,减少不必要的数据传输,加快页面加载速度。
- 用户体验: 确保在各种设备和屏幕尺寸上都能获得最佳的视觉效果。
- 灵活性: 通过媒体查询和宽度描述符,提供了一种非常灵活的图片资源管理方式,允许精细控制图片在不同条件下的选择逻辑。
懒加载
当一个网站图片数量太多,会造成很大的性能消耗。而有时候页面还没拉到下面,且用户有可能不浏览下面的数据,此时那些图片加载的性能消耗就被浪费掉了。
图片懒加载的实现原理就是在用户页面滑动到该图片的位置时再去加载图片。要想实现该功能,需要了解如下的前置知识。
前置知识
自定义指令
在 vue2
中,自定义指令的创建方式为通过 directives
关键字设置自定义指令,其中分为全局注册和局部注册:
局部注册
vue<template> <div id="home"> <input type="text" v-focus="daodao" /> </div> </template> <script> export default { name: "home", directives: { focus: { //参数一:当前元素,参数二:绑定的一些相关信息,参数三:虚拟dom节点,参数四:上一个拟dom bind(el, binding, vNode, oldvNode) { el.focus(); // 元素获焦 el.value = binding.value; // 把值赋值上去 }, }, }, }; </script>
全局注册
js//注册全局指令 Vue.directive("demo", { //自定义指令钩子函数 // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.focus(); }, });
第二个参数是一个对象,里面包含每个生命周期钩子函数。如果只关心
bind
和update
时触发相同行为,而不关心其它的钩子。可以这样写:jsVue.directive("color-swatch", function (el, binding) { el.style.backgroundColor = binding.value; });
在 vue3
中,自定义指令需要这样写:
局部注册
vue<script setup> // 在模板中启用 v-focus const vFocus = { mounted: (el) => el.focus(), }; </script> <template> <input v-focus /> </template>
全局注册
jsconst app = createApp({}); // 使 v-focus 在所有组件中都可用 app.directive("focus", { /* ... */ });
同样的,
vue3
自定义指令的参数二也支持简写形式,如果是仅仅需要在mounted
和updated
上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:html<div v-color="color"></div>
jsapp.directive("color", (el, binding) => { // 这会在 `mounted` 和 `updated` 时都调用 el.style.color = binding.value; });
页面元素可视
MDN 这么描述 IntersectionObserver() 方法:
IntersectionObserver
接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。当一个
IntersectionObserver
对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver
被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。
翻译成大白话来说就是,它可以判断元素是否出现在可视区域中的方法。其中它的参数中 intersectionRatio
属性小于等于 0 则代表它还未出现在可视区域内。
代码实现
实现步骤如下:
- 为图片绑定一个自定义指令,把是否需要懒加载的参数传过去。再通过自定义属性
data-
保存该图片的路径 - 在元素挂载到页面上的钩子函数中判断该图片是否需要懒加载
- 不需要懒加载,直接获取图片路径并赋值渲染
- 需要懒加载,把本地保存的占位图片路径赋值过去
- 通过
new IntersectionObserver()
方法中的observe()
判断当前图片节点是否在可视区域,在可视区域内再获取图片的自定义属性并赋值 - 通过
unobserve()
取消监听图片节点
<template>
<div class="box">
<div class="wrapper">
<img v-lazy="{ nolazy }" :data-src="src" v-bind="$attrs" class="image" />
</div>
</div>
</template>
<script>
function lazyBinding(el, binding) {
const placehold =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
const { nolazy } = binding.value;
if (nolazy) return (el.src = el.dataset.src || placehold);
el.src = placehold;
const obServer = new IntersectionObserver((entries) => {
// 如果 intersectionRatio 为 0,则目标在视野外,
// 我们不需要做任何事情。
if (entries.find((v) => v.intersectionRatio)) {
el.src = el.dataset.src || placehold;
obServer.unobserve(el);
}
});
obServer.observe(el);
}
export default {
inheritAttrs: false,
props: {
lazy: {
type: Boolean,
default: true,
},
src: {
type: String,
default: "",
},
},
computed: {
nolazy() {
return !this.lazy;
},
},
directives: {
lazy: {
bind(el, binding) {
lazyBinding(el, binding);
},
componentUpdated(el, binding) {
lazyBinding(el, binding);
},
},
},
};
</script>
<style scoped lang="scss">
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
background-color: rgb(168, 210, 255);
&.show {
animation: fade 0.5s ease-in-out;
}
}
</style>
封装成一个子组件,方便后续父组件复用。
关键图片预加载
代码
如果页面上有视觉上非常重要的图片(如 banner 图),可以使用 <link rel="preload" as="image">
在 HTML 头部预加载这些图片。这样可以确保当用户访问页面时,关键内容能够尽快显示。
示例代码如下:
<link rel="preload" href="important-image.jpg" as="image" />
好处
- 提高性能: 预加载可以确保关键图片在 HTML 解析过程中尽早开始下载,减少了页面完全加载所需的时间。
- 改善用户体验: 对于用户视觉上非常重要的图片,如页面顶部的 banner 图或关键的产品图片,预加载可以确保它们能够快速显示,减少用户的等待时间。
- 更好的资源管理: 预加载提供了一种机制,允许开发者更精确地控制资源的加载顺序和时机。
最佳实践
优先考虑关键资源: 只预加载对用户体验影响最大的图片。过多的预加载可能会浪费带宽,尤其是对于移动用户而言。
限制预加载数量: 避免同时预加载过多资源,以免占用过多的网络带宽和资源,影响到其他资源的加载。
使用媒体查询优化:
通过
media
属性,可以根据设备的屏幕大小或分辨率条件性地预加载图片,优化跨设备体验。html<link rel="preload" href="large-hero.jpg" as="image" media="(min-width: 600px)" />
考虑浏览器兼容性: 尽管
<link rel="preload">
在现代浏览器中得到了广泛支持,但仍有一些旧版本浏览器不支持。为这些用户提供回退方案或保证网页也能在不预加载的情况下正常使用。
浏览器协商缓存
通过合理配置 HTTP 缓存头(如 Cache-Control),可以使浏览器缓存已加载的图片,避免在用户再次访问时重新下载,从而提高页面加载速度。
Fetch API 加载 Blob 图片
通过 Fetch API 获取图片的 Blob,并利用 URL.createObjectURL
创建一个可访问的 URL 地址,可以实现先加载一个小尺寸的图片作为预览,待页面其他内容加载完成后再替换为高分辨率的图片。
首先使用 Fetch API 请求低分辨率图片的 Blob。
使用 URL.create0bjectURL
创建临时 URL,并将其设置为图片的 src。页面其他内容加载完成后,重复上述过程加载高分辨率图片并替换之。 当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL()
方法来释放。
注意
URL.createObjectURL
方法会造成一个缓存,用户短时间内可以通过缓存多次查看下载好的图片,但是后期不需要的时候需要释放掉,避免内存泄漏。
Caches API
Caches API 允许开发者将网络请求及其响应对象存储起来,用于未来的请求。
通过预缓存关键资源,可以实现即使在离线状态下也能快速加载页面。
响应式图片处理(使用 picture 元素配合 source 元素)
除了使用 srcset
和 sizes
属性外,还可以使用 picture
元素配合 source
元素定义不同情境下应加载的图片资源。这种方法提供了更灵活的图片响应式处理能力,允许基于设备特性(如屏幕宽度、分辨率、网络条件)选择最合适的图片。
<picture>
<source media="(min-width: 800px)" srcset="large.jpg" />
<source media="(min-width: 450px)" srcset="medium.jpg" />
<img src="small.jpg" alt="示例图片" />
</picture>
图片压缩和优化-如 WebP 图片格式
webp
格式的图片是针对 web
浏览器性能做了特殊优化的图片格式,与 jpg
和 png
等格式相比,图片效果差不多,但是解码速度以及大小尺寸会更小。
在上传图片之前,可以使用工具(如 Sharp、Image0ptim、TinyPNG 等)手动或自动进行图片压缩。
这种方法可以在不影响视觉质量的前提下减少图片文件的大小从而减少加载时间。
HEIC 图片解码(wasm WebWorker)
HEIC
是一种高效的图片格式,尤其适用于 IOS 设备。虽然它提供了比 JPEG
更好的压缩效果,但浏览器的支持度不高。
通过 JavaScript 库如 heic2any 可以实现 HEIC
格式到 WebP
或 JPEG
的转换,使其能够在网页中使用。
canvas 优化-直接使用 rgba 绘制
对性能和速度要求很高的情况下可以考虑此种方式:wasm + canvas rgba
webgl 优化(GPU 加速)
WebGL(Web Graphics Library)是一种在任何兼容的网页浏览器中使用 GPU(图形处理单元)加速渲染图形的技术。
它是一个为网页内容提供强大的 3D 绘图 API 的 JavaScript API,基于 OpenGLES 2.0,能够在不需要插件的情况下在 HTML5 <canvas>
元素上进行高性能的图形渲染。
对于前端图片优化而言,WebGL 提供了比传统的 HTML 或 CSS 方法更丰富和高效的图像处理能力,特别是在进行图像滤镜、图形变换、视觉效果等高级功能时。