常见优化
大约 7 分钟
常见优化
路由懒加载
- SPA 项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,当用户打开首页时,会一次性加载所有的资源,造成首页加载很慢,降低用户体验
- 实现原理:ES6 的动态地加载模块——import(),调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue")
组件懒加载
- 页面的 JS 文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页);需要一定条件下才触发(比如弹框组件);复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 JS 文件大小
合理使用 Tree shaking
- tree-shaking 依赖于 ES6 的模块特性,ES6 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础
- 但如果 export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效
骨架屏优化白屏时长
- 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
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>
`,
})
虚拟滚动
- 虚拟滚动的插件有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized 等
// 安装插件
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>
Web Worker 优化长任务
- 由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况
- 当任务的运算时长 - 通信时长 > 50ms,推荐使用 Web Worker (通信时长:新建一个 web worker, 浏览器会加载对应的 worker.js 资源,也叫加载时长)
requestAnimationFrame 制作动画
setTimeout/setInterval、requestAnimationFrame 三者的区别:
- 引擎层面 setTimeout/setInterval 属于 JS 引擎,requestAnimationFrame 属于 GUI 引擎 JS 引擎与 GUI 引擎是互斥的,也就是说 GUI 引擎在渲染时会阻塞 JS 引擎的计算
- 时间是否准确 requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout/setInterval 是宏任务,根据事件轮询机制,其他任务会阻塞或延迟 js 任务的执行,会出现定时器不准的情况
- 性能层面 当页面被隐藏或最小化时,setTimeout/setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
图片优化
图片的动态裁剪,使图片变小
图片懒加载,data-xxx 属性,vue-lazyload
使用字体图标,iconfont,一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,减少了 http 请求,可以随意的改变颜色、产生阴影、透明效果、旋转,几乎支持所有的浏览器
图片转 base64 格式,减少 http 请求,处理小图片,url-loader
// 安装 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 } }] }] } };
图片预加载
// main.js
import { preload } from "@/plugins/preload.js";
import PreloadImage from "@/components/PreloadImage.vue"; //图片预加载组件
Vue.component(PreloadImage.name, PreloadImage);
//图片预加载方法
preload();
// preload.js
import store from "store";
const createImageElement = (image_src) => {
const imageElement = new Image();
imageElement.setAttribute("src", image_src);
imageElement.setAttribute("alt", "");
return imageElement;
};
export const preload = () => {
const resources = require.context("@/assets/images/preload", false, /\.(png|jpg|PNG|JPG)$/); // 需要预加载的图片放在指定文件夹
resources
.keys()
.filter((_) => !_.includes("assets/images/preload"))
.forEach((absolute_src) => {
let image_name = absolute_src.replace(/^\.\//, "").replace(/\.\w+$/, "");
let image_element = createImageElement(resources(absolute_src));
store.commit("SET_PRELOAD_IMAGE", { image_name, image_element });
});
};
// store.js
state:{
preloadImageObject:{}
}
mutations: {
SET_PRELOAD_IMAGE(state, { image_name, image_element }) {
state.preloadImageObject[image_name] = image_element;
},
},
// PreloadImage.vue
<template>
<div class="preload-image" ref="imageWrap"></div>
</template>
<script>
export default {
name: "PreloadImage",
props: {
imageName: String,
},
computed: {
preloadImageObject() {
return this.$store.state.preloadImageObject;
},
currentImage() {
return this.preloadImageObject[this.imageName] || null;
},
},
mounted() {
console.log("mounted:");
this.$nextTick(() => {
this.$refs.imageWrap && this.$refs.imageWrap.appendChild(this.currentImage);
});
},
beforeDestroy() {
this.$refs.imageWrap && this.$refs.imageWrap.removeChild(this.currentImage);
},
};
</script>
echarts 异步加载
<template>
<div class="app-container">
<div class="charts">
<div v-for="item in domList" :id="item" :key="item" class="chart" />
</div>
</div>
</template>
<script>
const echarts = require("echarts");
const chartNum = 1000; // 图表数量
const MAX_CURRENT = 50; // 图表最大渲染并发数
const chartIntervalTime = 2000; // 图表定时渲染毫秒数
let executing = [];
/**
* @params {Number} poolLimit -最大并发限制数
* @params {Array} array -所有的并发请求|渲染数组
* @params {Function} iteratorFn -对应执行的并发函数(接受 array 的每一项值)
*/
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // 所有执行中的 promises
executing = []; // 正在执行中的 promises
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item));
ret.push(p);
if (array.length >= poolLimit) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) await Promise.race(executing);
}
}
return Promise.all(ret);
}
export default {
data() {
return {
domList: [],
chartObjs: {},
chartData: [150, 230, 224, 218, 135, 147, 260],
};
},
mounted() {
// 创建echart并绘图
this.createChart();
// 隔3秒更新图表数据并渲染
this.intervalChartData(chartIntervalTime);
},
methods: {
// 创建echart并绘图
async createChart() {
for (let i = 1; i <= chartNum; i++) {
this.domList.push("chart" + i);
}
this.$nextTick(this.renderChartList);
},
async renderChartList() {
const res = await asyncPool(MAX_CURRENT, this.domList, (i, arr) => {
return new Promise(async (resolve) => {
const res = await this.initChart(i);
resolve(res);
}).then((data) => {
console.log(data);
return data;
});
});
},
// 隔3秒更新图表数据并渲染
intervalChartData(s) {
setInterval(() => {
if (executing.length > 0) return; // 还有正在执行的渲染 不重复添加
this.renderChartList();
}, s);
},
// 初始化图表
initChart(domId) {
return new Promise((resolve) => {
if (!this.chartObjs[domId]) {
this.chartObjs[domId] = echarts.init(document.getElementById(domId));
}
const option = {
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
},
yAxis: {
type: "value",
},
series: [
{
data: this.chartData,
type: "line",
},
],
};
this.chartObjs[domId].clear();
this.chartObjs[domId].setOption(option);
this.chartObjs[domId].on("finished", () => {
resolve(domId);
});
});
},
},
};
</script>
<style scoped>
.chart {
float: left;
width: 360px;
height: 300px;
margin: 10px;
border: 2px solid #ff9900;
}
</style>
js 控制并发数量
// es6
function asyncPool(poolLimit, array, iteratorFn) {
let i = 0
const ret = [] // 存储所有的异步任务
const executing = [] // 存储正在执行的异步任务
const enqueue = function () {
if (i === array.length) {
return Promise.resolve()
}
const item = array[i++] // 获取新的任务项
const p = Promise.resolve().then(() => iteratorFn(item, array))
ret.push(p)
let r = Promise.resolve()
// 当poolLimit值小于或等于总任务个数时,进行并发控制
if (poolLimit <= array.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
if (executing.length >= poolLimit) {
r = Promise.race(executing)
}
}
// 正在执行任务列表 中较快的任务执行完成之后,才会从array数组中获取新的待办任务
return r.then(() => enqueue())
}
return enqueue().then(() => Promise.all(ret))
}
// es7
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = [] // 存储所有的异步任务
const executing = [] // 存储正在执行的异步任务
for (const item of array) {
// 调用iteratorFn函数创建异步任务
const p = Promise.resolve(iteratorFn(item))
ret.push(p) // 保存新的异步任务
// 当poolLimit值小于或等于总任务个数时,进行并发控制
if (poolLimit <= array.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e) // 保存正在执行的异步任务
if (executing.length >= poolLimit) {
// 一旦正在执行的promise列表数量等于限制数,就使用Promise.race等待某一个promise状态发生变更,
// 状态变更后,就会执行上面then的回调,将该promise从executing中删除,
// 然后再进入到下一次for循环,生成新的promise进行补充
await Promise.race(executing) // 等待较快的任务执行完成
}
}
}
return Promise.all(ret)
}
// es9
// for await (const value of asyncPool(concurrency, iterable, iteratorFn)) {
// ...
// }
async function* asyncPool(concurrency, iterable, iteratorFn) {
const executing = new Set()
async function consume() {
const [promise, value] = await Promise.race(executing)
executing.delete(promise)
return value
}
for (const item of iterable) {
// Wrap iteratorFn() in an async fn to ensure we get a promise.
// Then expose such promise, so it's possible to later reference and
// remove it from the executing pool.
const promise = (async () => await iteratorFn(item, iterable))().then(value => [promise, value])
executing.add(promise)
if (executing.size >= concurrency) {
yield await consume()
}
}
while (executing.size) {
yield await consume()
}
}
// eg:
const timeout = i => {
console.log('开始', i)
return new Promise(resolve =>
setTimeout(() => {
resolve(i)
console.log('结束', i)
}, i)
)
}
asyncPool(2, [5000, 4000, 3000, 2000], timeout).then(res => {
console.log(res)
})