最近沉迷逛某蓝色软件,收益良多!万分感谢博主 海阔_天空,写的太棒了👍🎉
下面是原文链接,我在原文的基础上浅做个笔记,方便个人快速复习
前端性能优化——首页资源压缩63%、白屏时间缩短86% - 掘金提升首屏的加载速度,是前端性能优化中最重要的环节,这里笔者梳理出一些 `常规且有效` 的首屏优化建议 通过对比优化前后的性能变化,来验证方案的有效性,了解并掌握其原理https://juejin.cn/post/7188894691356573754#heading-6
目录
1、路由懒加载
打包对比
实现原理 import()(分离 chunk)
webpackChunkName(指定打包时生成的代码块名)
路由懒加载 - 示例
2、组件懒加载
为啥用组件懒加载
组件懒加载 - 示例
组件懒加载 - 使用场景(条件触发的组件、公共组件、大体积 .js)
3、Tree shaking
作用(删除无用代码)
原理(ES6 可静态分析)
局限(只对用 export 导出的变量生效)
4、骨架屏
原理(将骨架屏内容放到 html 文件的根节点中)
骨架屏插件(vue-skeleton-webpack-plugin)
5、虚拟滚动
原理(监听 scroll,根据 scrollTop 截取 list 并展示)
虚拟滚动插件
6、Web Worker 优化长任务
控制台查看长任务(执行时间超过 50ms 的任务)
Web worker 举例(计算 20w 条数据)
Web Worker 通信时长
7、requestAnimationFrame GUI 定时器
是什么?有什么作用?(解决动画卡顿)
定时器区别(引擎、时间准确度、性能)
8、JavaScript 的加载方式
1)正常模式(阻塞DOM)
2)async 模式(不阻塞DOM,加载无序)
3)defer 模式(不阻塞DOM,加载有序)
4)module 模式(不阻塞DOM,加载有序)
5)preload 模式(link 属性,首页[关键]资源提前加载)
6)prefetch 模式(link 属性,非首页[关键]资源提前加载)
9、图片优化
动态裁剪图片(压缩裁剪)
图片懒加载(到可视区域再加载)
使用字体图标(体积小、样式灵活、兼容性好)
图片转 base64 格式(减少 http 请求)
10、参考文章
1、路由懒加载
打包对比
普通路由 vs 路由懒加载,在 SPA 项目中:
- 若采用普通路由,会把所有页面打包成一个文件,一次性加载所有资源
- 若将路由改成懒加载,会把单个路由页面打包成多个文件,体积大幅减小
实现原理 import()(分离 chunk)
路由懒加载实现原理:
- 通过 ES6 动态加载模块 import() 实现
- 调用 import() 之处,被作为分离的模块起点,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中
webpackChunkName(指定打包时生成的代码块名)
webpackChunkName:是一个注释,用于指定 webpack 在打包时生成的代码块的名称。它的作用是让 webpack 在打包时,将具有相同名称的模块打包到同一个代码块中,从而实现代码分割和按需加载的效果;如果不指定名字,则生成的文件名将不够语义化
路由懒加载 - 示例
下面是设置 路由懒加载 的例子:
// 通过 webpackChunkName 设置分割后代码块的名字
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");
const MetricGroup = () => import(/* webpackChunkName: "metricGroup" */ "@/views/metricGroup/index.vue");
const routes = [
{
path: "/",
name: "home",
component: Home
},
{
path: "/metricGroup",
name: "metricGroup",
component: MetricGroup
},
]
2、组件懒加载
为啥用组件懒加载
以 公共组件-弹框 为例,如果在多个页面中引入,则打包时会被重复打包进多个文件
同时,弹框组件不是一进入页面就加载,而是需要用户手动触发显示
此时,就应该考虑 组件懒加载 了
组件懒加载 - 示例
弹框组件懒加载示例:
<script>
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo');
export default {
name: 'homeView',
components: {
dialogInfo
}
}
</script>
重新打包后:
- 引入 dialogInfo 组件的页面中,不会被加入 dialogInfo 组件的代码
- dialogInfo 组件被独立打包成 dialogInfo.js,当用户点击按钮时,才会去加载 dialogInfo.js 和 dialogInfo.css
组件懒加载 - 使用场景(条件触发的组件、公共组件、大体积 .js)
组件懒加载的使用场景:
- 该组件不是一进入页面就展示,需要一定条件下才触发(比如弹框组件)
- 该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 JS 文件大小(比如表格组件、图形组件等)
- 该页面的 JS 文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页)
PS:资源拆分的过细也不好,会造成浏览器 http 请求增多
3、Tree shaking
作用(删除无用代码)
Tree shaking 的作用:消除无用的 JS 代码,减少代码体积
举个🌰:项目中只用了 targetType(),未使用 deepClone();则 deepClone() 不会被打包
// util.js
export function targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
原理(ES6 可静态分析)
依赖于 ES6 的模块特性,ES6 模块依赖关系是确定的,可以进行静态分析,和运行时状态无关(也就是说,不用执行代码,就能判断啥用了啥没用),这就是 tree-shaking 的基础
CommonJS 是动态加载,执行后才知道引用的什么模块,不能通过静态分析去优化
局限(只对用 export 导出的变量生效)
并不是说所有无用的代码都可以被消除,下面的 deepClone() 仍然会被打包
// util.js
export default {
targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
},
deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
};
// 引入并使用
import util from '../util';
util.targetType(null)
原因分析:
- export default 导出的是一个对象,无法通过 静态分析 判断对象的哪些属性变量未被使用
- tree-shaking 只对用 export 导出的变量生效,这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一
4、骨架屏
使用骨架屏,可以缩短 FP 白屏时间(高达86%),提升用户体验
原理(将骨架屏内容放到 html 文件的根节点中)
SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,需要通过加载 JS 将内容挂载到根节点上,造成长时间的白屏
骨架屏插件基于这种原理,在项目打包时,将骨架屏内容放到 html 文件的根节点中(根节点内部为骨架屏)
使用骨架屏插件,打包后的 html 文件:
<div id="app" >
// 根节点内部为骨架屏
<div class="skeleton loading">
<div class="header"></div>
<div class="content"></div>
</div>
</div>
骨架屏插件(vue-skeleton-webpack-plugin)
这里以 vue-skeleton-webpack-plugin 插件为例,可以给 不同页面 设置不同骨架屏
安装
npm i vue-skeleton-webpack-plugin
vue.config.js 配置
// 骨架屏
const SkeletonWebpackPlugin = require("vue-skeleton-webpack-plugin");
module.exports = {
configureWebpack: {
plugins: [
new SkeletonWebpackPlugin({
// 实例化插件对象
webpackConfig: {
entry: {
app: path.join(__dirname, './src/skeleton.js') // 引入骨架屏入口文件
}
},
minimize: true, // SPA 下是否需要压缩注入 HTML 的 JS 代码
quiet: true, // 在服务端渲染时是否需要输出信息到控制台
router: {
mode: 'hash', // 路由模式
routes: [
// 不同页面可以配置不同骨架屏
// 对应路径所需要的骨架屏组件id,id的定义在入口文件内
{ path: /^\/home(?:\/)?/i, skeletonId: 'homeSkeleton' },
{ path: /^\/detail(?:\/)?/i, skeletonId: 'detailSkeleton' }
]
}
})
]
}
}
新建 skeleton.js 入口文件
// skeleton.js
import Vue from "vue";
// 引入对应的骨架屏页面
import homeSkeleton from "./views/homeSkeleton";
import detailSkeleton from "./views/detailSkeleton";
export default new Vue({
components: {
homeSkeleton,
detailSkeleton,
},
template: `
<div>
<homeSkeleton id="homeSkeleton" style="display:none;" />
<detailSkeleton id="detailSkeleton" style="display:none;" />
</div>
`,
});
5、虚拟滚动
原理(监听 scroll,根据 scrollTop 截取 list 并展示)
只渲染可视区域的列表项,非可视区域的不渲染,滚动时 动态更新 可视区域
虚拟滚动原理:
- 计算出 totalHeight 列表总高度
- 触发滚动事件时,根据 scrollTop 值不断更新 startIndex 以及 endIndex
- 根据 startIndex 以及 endIndex ,从列表数据 listData 中截取对应元素,并在可视区域展示
虚拟滚动好处:在渲染 10w 个文本节点的情况下,使用虚拟滚动可以缩短 78% 白屏时长
虚拟滚动插件
虚拟滚动的插件有很多,比如:
- vue-virtual-scroller
- vue-virtual-scroll-list
- react-tiny-virtual-list
- react-virtualized
- ...
简单介绍 vue-virtual-scroller 的使用,该插件主要有 RecycleScroller.vue、DynamicScroller.vue 两个组件:
- RecycleScroller 需要 item 的高度为静态的,也就是列表每个 item 的高度都是一致的
- DynamicScroller 可以兼容 item 的高度为动态的情况
// 安装插件
npm install vue-virtual-scroller
// main.js
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
Vue.use(VueVirtualScroller)
// 使用
<template>
<RecycleScroller
class="scroller"
:items="list"
:item-size="32"
key-field="id"
v-slot="{ item }">
<div class="user"> {{ item.name }} </div>
</RecycleScroller>
</template>
6、Web Worker 优化长任务
控制台查看长任务(执行时间超过 50ms 的任务)
浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系
当页面中有很多长任务(执行时间超过 50ms 的任务)时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况
查看页面的长任务:
- 打开控制台,选择 Performance 工具
- 点击 Start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务
Web worker 举例(计算 20w 条数据)
把下方代码丢到 主线程 中执行,计算过程中,页面一直处于卡死状态,无法操作
let sum = 0;
for (let i = 0; i < 200000; i++) {
for (let i = 0; i < 10000; i++) {
sum += Math.random()
}
}
使用 Web Worker 执行上述代码时,计算过程中,页面正常可操作、无卡顿
// worker.js
onmessage = function (e) {
// onmessage获取传入的初始值
let sum = e.data;
for (let i = 0; i < 200000; i++) {
for (let i = 0; i < 10000; i++) {
sum += Math.random()
}
}
// 将计算的结果传递出去
postMessage(sum);
}
Web Worker 具体的使用与案例,详情见下方文章:
一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥https://juejin.cn/post/7137728629986820126
Web Worker 通信时长
并不是执行时间超过 50ms 的任务,就可以使用 Web Worker,还要考虑 通信时长
举个🌰:一个运算执行时长为 100ms,但是通信时长为 300ms, 用了 Web Worker 可能会更慢
当任务的运算时长 - 通信时长 > 50ms,推荐使用 Web Worker
新建一个 Web worker,浏览器会加载对应资源,下图中的 Time 是这个资源的通信时长(也叫加载时长)
7、requestAnimationFrame GUI 定时器
是什么?有什么作用?(解决动画卡顿)
requestAnimationFrame 是浏览器专门为动画提供的 API,功能类似于 setTimeout/setInterval
可以使用 requestAnimationFrame 来执行一些需要高性能的操作,例如动画、滚动、拖拽等
requestAnimationFrame 的刷新频率,与显示器的刷新频率保持一致,因此,使用 requestAnimationFrame 可以解决用 setTimeout/setInterval 制作动画卡顿的情况
定时器区别(引擎、时间准确度、性能)
setTimeout/setInterval、requestAnimationFrame 区别:
- 引擎层面:setTimeout/setInterval 属于 JS 引擎,requestAnimationFrame 属于 GUI 引擎;JS 引擎与 GUI 引擎互斥,也就是说,GUI 引擎在渲染时,会阻塞 JS 引擎的计算
- 时间是否准确:setTimeout/setInterval 是宏任务,根据事件轮询机制,其他任务会阻塞或延迟 js 任务的执行,会出现定时器不准的情况;requestAnimationFrame 刷新频率是固定且准确的
- 性能层面:当页面被隐藏或最小化时,setTimeout/setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
8、JavaScript 的加载方式
1)正常模式(阻塞DOM)
正常模式下,JS 会阻塞 dom 渲染
浏览器必须等待 index.js 加载和执行完成后,才能去做其它事情
<script src="index.js"></script>
2)async 模式(不阻塞DOM,加载无序)
<script async src="index.js"></script>
async 模式下,JS 的加载是异步的,不会阻塞 DOM 渲染
async 加载是无顺序的,当它加载结束,JS 会立即执行
使用场景:若该 JS 与 DOM 元素没有依赖关系,可以使用 async 模式,比如埋点统计
3)defer 模式(不阻塞DOM,加载有序)
defer 可以用来控制 JS 文件的执行顺序,比如 element-ui.js 和 vue.js
因为 element-ui.js 依赖于 vue,所以必须先引入 vue.js,再引入 element-ui.js
<script defer src="vue.js"></script>
<script defer src="element-ui.js"></script>
defer 模式下,JS 的加载是异步的,不会阻塞 DOM 渲染
defer 资源会在 DOMContentLoaded 执行之前,如果有多个设置了 defer 的 script 标签存在,则会按照引入的前后顺序执行,即便是后面的 script 资源先返回
使用场景:一般情况下都可以使用 defer,特别是需要控制资源加载顺序时
4)module 模式(不阻塞DOM,加载有序)
<script type="module">import { a } from './a.js'</script>
在主流的现代浏览器中,script 标签的属性可以加上 type="module",浏览器会对其内部的 import 引用发起 HTTP 请求,获取模块内容
这时 script 的行为会像是 defer 一样,在后台下载,并且等待 DOM 解析
Vite 就是利用浏览器支持原生的 es module 模块,开发时跳过打包的过程,提升编译效率
5)preload 模式(link 属性,首页[关键]资源提前加载)
<link rel="preload" as="script" href="index.js">
link 标签的 preload 属性:提前加载某些依赖
preload 特点:
- preload 加载的资源,是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件;
- preload 加载的 JS 脚本,加载完成后不会立即调用,而是等到需要时再调用;
vue2 项目打包生成的 index.html 文件,会给 首页资源 全部添加 preload,实现关键资源的提前加载
6)prefetch 模式(link 属性,非首页[关键]资源提前加载)
<link rel="prefetch" as="script" href="index.js">
link 标签的 prefetch 属性:利用浏览器的空闲时间,加载页面将来可能用到的资源
prefetch 特点:
- pretch 加载的资源,可以获取非当前页面的资源,并将其放入缓存至少5分钟(无论资源是否可以缓存)
- 当页面跳转时,未完成的 prefetch 请求不会被中断
可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度
PS:现代框架已经将 preload、prefetch 添加到打包流程中了,可以通过灵活的配置,去使用预加载功能
9、图片优化
如何去压缩图片,让图片更快的展示出来,有很多优化工作可以做(比如淘宝首页的图片资源都很小)
动态裁剪图片(压缩裁剪)
经过动态裁剪后的图片,可能会从 1.8M 降低至 12.8KB,加载速度显著提升
很多云服务,比如 阿里云 或 七牛云,都提供了图片的动态裁剪功能
只需在图片的 url 地址上动态添加参数,就可以得到你所需要的尺寸大小,比如:
http://7xkv1q.com1.z0.glb.clouddn.com/grape.jpg?imageView2/1/w/200/h/200
图片懒加载(到可视区域再加载)
JavaScript 图片懒加载 - Web前端工程师面试题讲解_哔哩哔哩_bilibiliJavaScript 图片懒加载 - Web前端工程师面试题讲解, 视频播放量 35335、弹幕量 150、点赞数 1291、投硬币枚数 899、收藏人数 1643、转发人数 107, 视频作者 技术蛋老师, 作者简介 技能和观点的长期分享,相关视频:转行前端后的第一次技术面试,凉凉咯,图片懒加载和预加载,Vue怎么做图片懒加载?,Async Await关键字 - 让我们更优化地写代码 - JavaScript前端Web工程师,面试官:假如有十万条数据,前端应该怎么处理?分页渲染?no,你应该回答这个...,前端面试:说几种图片懒加载的实现方式。追问:怎么知道是否进入可视区域了?这几点必说……,Cookie、Session、Token究竟区别在哪?如何进行身份认证,保持用户登录状态?,彻底搞懂图片懒加载和预加载,公司昨天入职的211大学毕业的程序员,昨天看了项目代码,今天就开始写了,而且还没有报错,真的很厉害,JavaScript基础语法-dom-bom-js-es6新语法-jQuery-数据可视化echarts黑马pink老师前端入门基础视频教程(500多集)持续https://www.bilibili.com/video/BV1FU4y157Li/?spm_id_from=333.999.0.0
什么是图片懒加载?
对于一些图片量较大的首页,只需呈现可视区域的图片,当用户滑动页面时,再去加载新的
图片懒加载原理?
- 浏览器会自动对页面中的 img 标签的 src 属性 发送请求 并下载图片
- 通过 html5 自定义属性 data-xxx 暂存 src 的值
- 图片出现在可视区域时,再将 data-xxx 赋值给 img 的 src 属性
<img src="" alt="" data-src="./images/1.jpg">
<img src="" alt="" data-src="./images/2.jpg">
这里以 vue-lazyload 插件为例
// 安装
npm install vue-lazyload
// main.js 注册
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 配置项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png', // 图片加载失败时的占位图
loading: 'dist/loading.gif', // 图片加载中时的占位图
attempt: 1
})
// 通过 v-lazy 指令使用
<ul>
<li v-for="img in list">
<img v-lazy="img.src" :key="img.src" >
</li>
</ul>
使用字体图标(体积小、样式灵活、兼容性好)
字体图标优点:
- 轻量级:一个图标字体要比一系列的图像小
- 灵活性:可以随意改变样式
- 兼容性:几乎支持所有的浏览器
图片转 base64 格式(减少 http 请求)
将小图片转换为 base64 编码字符串,并写入 HTML 或者 CSS 中,减少 http 请求
转 base64 格式的优缺点:
- 善于处理非常小的图片,因为 Base64 编码后,图片大小会膨胀为原文件的 4/3,如果对大图也使用 base64 编码,后者体积会明显增加,即便减少了 http 请求,也无法弥补增大文件体积带来的性能开销
- 在传输非常小的图片的时候,base64 带来的文件体积膨胀、以及浏览器解析 base64 的时间开销,与它节省掉的 http 请求开销相比,可以忽略不计
可以使用 url-loader 将图片转 base64:
// 安装
npm install url-loader --save-dev
// 配置
module.exports = {
module: {
rules: [{
test: /.(png|jpg|gif)$/i,
use: [{
loader: 'url-loader',
options: {
// 小于 10kb 的图片转化为 base64
limit: 1024 * 10
}
}]
}]
}
};
10、参考文章
路由懒加载原理及使用
vue-skeleton-webpack-plugin 骨架屏插件使用
前端性能优化-虚拟滚动
requestAnimationFrame制作动画
浅谈script标签中的async和defer
Tree-Shaking性能优化实践 - 原理篇
使用 Preload&Prefetch 优化前端页面的资源加载
github.com/xy-sea/blog
再次鸣谢博主 海阔_天空,写的太棒了👍🎉