1 路由懒加载
SPA单页面应用项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,当用户打开首页时,会一次性加载所有资源,造成首页加载很慢,降低用户体验。(前后大概降低30%)
js
// 请只在生产时懒加载,否则路由多起来后,开发时的构建速度感人
// 同步加载的路由
{
name: 'personHomeHome',
path: '/personHome/home',
meta: {
title: '个人首页',
breadcrumb: false,
dot: false,
icon: 'iconfont iconzhuye1',
},
component: (resolve) => require(['@/views/personHome/home/home.vue'], resolve),
},
// 异步加载
{
path: 'editor',
meta: {
requireLogin: true,
istrun:true
},
component: () => import(/* webpackChunkName: 'root' */ '@/views/form/editor')
},
2 组件懒加载
除了路由的懒加载外,组件的懒加载在很多场景下也有重要的作用
例如:当用户打开某个页面,会一次性加载该页面所有的资源,我们期望的是有些组件使用户触发按钮后,再加载该弹窗组件的资源,例如弹窗组件,这时候,就可以考虑用懒加载的方式引入
js
// 没有懒加载时候
import treeTable from "@/components/table/treeTable.vue";
import editDialog from "./editDialog.vue";
export default {
components: {
treeTable,
editDialog,
},
}
// 改成懒加载
const editDialog = ()=> import(/*webpackChunkName: "editDialog"*/ "./editDialog.vue")
const treeTable = ()=> import(/*webpackChunkName: "treeTable"*/ "@/components/table/treeTable.vue");
export default {
components: {
treeTable,
editDialog,
},
}
此时打包之后,弹窗组件就被单独打包出来editDialog对应的js 和css ,当用户点击按钮时,才会去加载。
组件懒加载的场景:
- 该页面的js文件体积大,导致页面打开很慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度。
- 该组件不是已进入页面就展示,需要一定条件下才触发(例如弹窗组件)
- 该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 js 文件大小(比如表格组件、图形组件)
3 优化分包策略
vue-cli3 的默认优化是将所有npm依赖都打进 chunk-vendor,但这种做法在依赖多的情况下导致 chunk-vendor 过大
splitChunks 优化分包
js
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: { // libs 缓存组
name: 'chunk-libs', // 该缓存组的名称 chunk-libs
test: /[\\/]node_modules[\\/]/, // [\\/] 匹配 / 或 \,匹配路径中包含node_modules的模块
priority: 10, // 缓存组的优先级,数字越小,优先级越高
chunks: 'initial' //初始的依赖模块 only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
}
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
config.optimization.runtimeChunk('single') // 为每个入口点生成一个运行时代码块
}
externals 配合CDN 加速
用 webpack 的externals 属性把把不需要打包的库文件分离出去,减少打包后的文件的大小,用cdn 资源的方式将第三方依赖包引入项目,可以大大减少项目打包体积
js
// 库文件内容准备
vue@2.7.14
// 配置 externals 对象的技巧
key: 就是 import AAA from 'BBB'中的 BBB
value: 就是可以在引入CDN 资源后,在控制台打印 window.XXX, value 就是 XXX
@1 CDN 引入vue
如果npm 已安装了vue ,可以选择卸载,不卸载也行,但是 vue-template-compiler 需要安装,注意版本号和vue 版本保持一致
yarn add vue-template-compiler@2.7.14 -S
// vue@2.7.14 // Vue 的版本号
public文件夹里的index.html 文件引入vue
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
@2 webpack.config.js中的配置
// 在这里配置后,减少了压缩的包内容,需要在public/index.html通过cdn方式再引入,注意对应的版本
chainWebpack (config) {
config.externals = {
vue: 'Vue',
},
}
// 然后项目里所有的import Vue from 'vue' 都可以去掉了,注意Vue是大写;
原理是通过 cdn 引入的vue 会挂载到 window 下面, window.Vue
4 合理使用 Tree shaking
Tree shaking的作用:消除无用的js 代码,减少代码体积
例如:
js
export function isFunction(obj) {
return typeof obj === "function" &&
typeof obj.nodeType !== "number" &&
typeof obj.item !== "function";
};
export function isPlainObject(obj) {
let proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true; //匹配 Object.create(null)
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
};
项目中只使用 isFunction 没有使用 isPlainObject 方法,项目打包后,isPlainObject 方法不会被打包到项目里
Tree shaking原理:
依赖于 ES6的模块特性, ES6 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析这是 Tree shaking的基础
静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。 ES6 之前的模块,比如 CommonJs 是动态加载,只有执行后才可以引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 Tree shaking 成为可能
Tree shaking并不是万能的
并不是所有无用的代码都可以被消除,还是上面的代码,换个写法 Tree shaking 就失效了
js
export default {
isFunction(obj) {
return typeof obj === "function" &&
typeof obj.nodeType !== "number" &&
typeof obj.item !== "function";
},
isPlainObject(obj) {
let proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true; //匹配 Object.create(null)
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
},
}
同样的,项目中只使用 isFunction 没有使用 isPlainObject 方法,项目打包后,isPlainObject 方法还是被打包到项目里,
原因是 export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree shaking 只对使用 export 单个导出的变量生肖
这也是函数式编程越来越被青睐的原因,因为可以很好利用 tree-shaking 精简项目的体积
5 骨架屏优化白屏时长
SPA单页面应用,无论是 vue 还是 react ,最初的 html 都是空白的,需要通过加载 js 将内容挂载到跟节点上,这套机制的副作用:会造成长时间的白屏
常见的骨架屏插件就是基于这种原理,在项目打包时,将骨架屏的内容直接放到 html 文件的根节点中
骨架屏插件 vue-skeleton-webpack-plugin 插件为例,该插件的亮点时可以给不同的页面设置不同的骨架屏
js
// 安装
yarn add 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>
`,
});
6 长列表虚拟滚动
首页中不乏有需要渲染长列表的场景,当渲染条数过多时,所需要的渲染时间会很长,滚动时还会造成页面卡顿,整体体验非常不好
虚拟滚动------指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的
虚拟滚动原理: 计算出 totalHeight 列表总高度,并在触发滚动事件时根据 scrollTop 指不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素
虚拟滚动插件vue 和react 中都有很多 vue-virtual-scroller、vue-virtual-scroll-list、react 中有 react-tiny-virtual-list、react-virtualized
这里简单介绍 vue-virtual-scroller 的使用
js
// 1. 安装插件
yarn add vue-virtual-scroller
// 2. 使用
// main.js 文件中
import VueVirtualScroller from 'vue-virtual-scroller' // 引入文件
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' // 引入样式
// 全局注册
Vue.use(VueVirtualScroller)
// 使用指定组件
import Vue from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
Vue.component('RecycleScroller', RecycleScroller)
// 页面使用
<template>
<RecycleScroller
class="scroller"
:items="list"
:item-size="32"
key-field="id"
v-slot="{ item }">
<div class="user"> {{ item.name }} </div>
</RecycleScroller>
</template>
// RecycleScroller可参考:https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller
该插件主要有 RecycleScroller 和 DynamicScroller 这两个组件,其中 RecycleScroller 需要设置 item 高度,也就是列表每项 item 的高度都是一致的, 而 DynamicScroller 可以兼容 item 的高度为动态的
7 Web Worker 优化长任务
长任务: 执行时间超过 50ms 的任务
由于浏览器 GUI 渲染线程与 js 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞。出现界面卡顿情况
查看页面的长任务:
打开控制台,选择 Performance 工具,点击 start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务
Web Worker的通信时长
并不是执行时间超过 50ms 的任务,就可以使用 Web Worker, 还需要考虑通信时长的问题,假如一个运算执行时长为100ms ,但是通信时长为300ms ,用了 Web Worker 可能会更慢。
所以当任务的运算时长 减去 通信时长 大于 50ms ,可以考虑使用 Web Worker
8 requestAnimationFrame 制作动画
requestAnimationFrame 是浏览器专门为动画提供的 API , 它的刷新频率与显示器的频率保持一致,使用该 API 以解决用 setTimeout/setInterval 制作动画卡顿的情况
requestAnimationFrame 和 setTimeout /setInterval 的区别
引擎层面
setTimeout /setInterval 属于 js 引擎, requestAnimationFrame 属于 GUI 引擎。 js 引擎与GUI 引擎 是互斥的,也就是说 GUI 引擎在渲染时会阻塞JS 引擎的计算
时间准确性
requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout /setInterval 是宏任务,根据时间轮询机制,其他任务会阻塞或延迟 js 任务的执行,会出现定时器不准的情况
性能方面
当页面被隐藏或最小化时,setTimeout /setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
9 优化js加载方式
js一般加载模式
js
<script src="./index.js"></script>
这种情况下 js 会阻塞 dom 渲染,浏览器必须等待 index.js 加载和执行完成后才能去做其他事情
async 模式
js
<script async src="./index.js"></script>
async 模式下,他是异步加载的,js 不会阻塞 DOM 的渲染, async 加载是无顺序的,当他加载结束,js 会立即执行
defer 模式
js
<script defer src="./index.js"></script>
defer 模式下, js 的加载也是异步的,defer 资源会在 DOMContentLoaded 执行之前且defer有顺序的加载
如果有多个设置了 defer 的 script 标签,则会按照引入的前后顺序执行,即便是后面的script 资源先返回
module 模式
js
<script type="module">
import { a } from './a.js'
</script>
在当代主流浏览器中, script 标签的属性可以加上 type="module",浏览器会对其内部的 import 引用发起 HTTP 请求,或者模块内容。这时 script 的行为会像是 defer 一样,在后台下载并且等待 DOM 解析
preload
link 标签的 preload 属性:用于提前加载一些需要的依赖,这些资源会优先加载。
js
<link rel="preload" as="script" href="index.js">
// 配置文件中 vue.config.js
// it can improve the speed of the first screen, it is recommended to turn on preload
config.plugin('preload').tap(() => [
{
rel: 'preload',
// to ignore runtime.js
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial'
}
])
vue2 项目打包生成的 index.html 文件,会自动给首页所有需要的资源,全部添加 preload ,实现关键资源的提前加载
preload 特点
- preload加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件
- preload加载的js 脚本其加载和执行的过程是分离的 既 preload 会预加载相应的脚本代码,待到需要时自行调用
prefetch
js
<link rel="prefetch" as="script" href="index.js">
prefetch 是利用浏览器的空闲时间,加载页面将来可能用到的资源的一种机制;通常可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度。
prefetch特点
- prefetch 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少5分钟(无论资源是否可以缓存)
- 当页面跳转时,未完成的 prefetch 请求不会被中断
10 图片优化
图片的动态裁剪
很多云服务,比如阿里云或七牛云阿里云或七牛云 都提供了图片的动态裁剪功能,效果很好,缺点需要花钱
只需在图片的url地址上动态添加参数,就可以得到你所需要的尺寸大小,比如:http://7xkv1q.com1.z0.glb.clouddn.com/grape.jpg?imageView2/1/w/100/h/100
图片懒加载
由于浏览器会自动对页面中的 img 标签的 src 属性发送请求并下载图片,可以通过 HTML5 自定义属性 data-xxx 先暂存 src 的值,然后再图片出现在屏幕可视窗口的时候,再将data-xxx的值重新赋值到 img 的src 属性即可
js
// 1.安装
yarn add vue-lazyload --save
// main.js 文件
import VueLazyload from 'vue-lazyload'
// Vue.use(VueLazyload) //无配置项
// 配置项
const loadimage = require('assets/img/common/loading.gif')
// const errorimage = require('assets/img/common/error.gif')
Vue.use(VueLazyload, {
preLoad: 1.3, //预加载的宽高比
loading: loadimage, //图片加载状态下显示的图片
// error: errorimage, //图片加载失败时显示的图片
attempt: 1, // 加载错误后最大尝试次数
})
// 使用 注意要加绑定key 【:key】
<ul>
<li v-for="img in list">
<img v-lazy="img.src" :key="img.src" >
</li>
</ul>
字体图标
图片转 base64