最近沉迷逛某蓝色软件,收益良多!万分感谢博主 海阔_天空,写的太棒了👍🎉
下面是原文链接,我在原文的基础上浅做个笔记,方便个人快速复习
目录
webpackChunkName(指定打包时生成的代码块名)
组件懒加载 - 使用场景(条件触发的组件、公共组件、大体积 .js)
骨架屏插件(vue-skeleton-webpack-plugin)
原理(监听 scroll,根据 scrollTop 截取 list 并展示)
7、requestAnimationFrame GUI 定时器
5)preload 模式(link 属性,首页[关键]资源提前加载)
6)prefetch 模式(link 属性,非首页[关键]资源提前加载)
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
图片懒加载(到可视区域再加载)
什么是图片懒加载?
对于一些图片量较大的首页,只需呈现可视区域的图片,当用户滑动页面时,再去加载新的
图片懒加载原理?
- 浏览器会自动对页面中的 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 骨架屏插件使用
使用 Preload&Prefetch 优化前端页面的资源加载
再次鸣谢博主 海阔_天空,写的太棒了👍🎉