综述
随着前后端分离架构流行,前端框架 Vue/React 大行其道,SPA(单页面应用)架构也越来越流行。
在 webpack 框架下前端性能优化也变的很简单。
以下是我基于 wbpack 给出的前端性能优化方案:
按照以下步骤对代码进行优化可以提升 60% 的页面加载性能
1、大文件分割
2、小文件聚合
3、异步加载
第一招 大文件变小
默认情况下,Webpack 会将所有代码构建成一个单独的包,这在小型项目通常不会有明显的性能问题,但伴随着项目的推进,包体积逐步增长可能会导致应用的响应耗时越来越长。
因此,怎么样合理的将大文件分割成小的文件就很重要了。
有同学或许有疑问,将大文件切分成小文件,文件内容总量没有减少,或许还会增加,而我的页面需要所有代码才能运行,这样切分的意义又在哪里呢?
这就要说到浏览器文件加载的机制了。浏览器并非单线程的,它是可以有 4 ~ 8 个并发的。也就是发出 4 ~ 8 个 http 请求的。也就是说它可以同时请求,4 ~ 8 个静态资源文件。同样的文件总量,使用 1 个 http 请求花销的时间,和使用 4 ~ 8 个请求分别获取花销的时间,哪个性能更高,自不必多说。
当然,也不是说所有的文件都需要拆分,视具体情况而定,假设你单文件总量本来就不高,加载速度本来就快就没必要折腾了。
webpack 切分文件的方案有两个,一个是 entry,一个是 splitChunks。entry 是针对多页面应用的,从不同的入口开始打包静态资源,对于多页面应用不用太关注。这里我们重点关注 splitChunks。
在模块化编程中,每一个模块就是一个 chunk。在项目中,我们的每一个文件都是模块。
chunk 分为两种,一种是 initial chunk。我们在使用 webpack 打包时,index.html 中初始引入的 main.js 就是这种 chunk。
另外一种叫 non-initial chunk,是可以延迟加载的块。比如动态导入的模块,或者通过 SplitChunksPlugin 配置的 chunk。
因此,使用动态导入和 SplitChunksPlugin 就是我们将大文件变小的重要方法。
动态导入 (import)
所谓的动态导入就是使用 webpack 实现的 import() 函数进行异步加载的方法。
import('./app.jsx').then((App) => {
ReactDOM.render(<App />, root);
});
PS: 这里的 import() 不是 ES6 模块化内容,ES6 模块化没有异步导入。
异步导入是 AMD 规范,这里的 import()函数是 webpack 自身实现的异步导入函数。
使用这个函数,webpack 将动态加载对应的模块,调用 import() 之处被视为分割点,意味着被请求的模块和它引用的所有子模块,会分割到一个单独的 chunk 中。
通过异步 import 打包后的文件, 文件名可以通过 Webpack 配置中的 config.output.chunkFilename 确定:
output: {
filename: '[name].js',
path: DIR_DIST,
chunkFilename: 'async.[id].js', // 此选项确定chunk文件的名称
}
也就是说通过 import()加载的模块会被单独打包成独立文件,而且不会一开始就加载到浏览器中,当代码运行到这里时,才会生成一个 script 标签添加到 head 中,然后监听 script 标签加载状态,onLoad 后返回 export 的内容。
因此,这里的 import()函数返回的是一个使用 Promise 包装的函数,可以通过 asncy/await 或者.then()来使用。
如果不使用 import()函数,那么所有通过 import ...from...导入的组件将全部打包到主文件中,因此可以看到 import()函数是 webpack 将大文件切分成小文件的利器。
关于异步加载的更多内容,我们在下文讨论,这里只说 import()函数分割文件的作用。
在框架中,我们通常会使用 import()函数做异步加载路由的配置,让不同的前端路由文件打包到不同的 js 文件中,模拟多页面输出。
分包机制 (SplitChunksPlugin)
目前 SplitChunksPlugin 已经整合到 webpack 的 optimization.splitChunks 中。
默认配置如下:
// webpack.config.js
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
具体说明可以参考 webpack 官网
官方配置虽然不能说是最好配置,但也是很具有代表性的。
比如一个 chunk 也就是一个文件控制在多大尺寸好呢?官方给出的是最少 20kb(minSize: 20000)这样是为了避免拆分出太多的小文件。当然,有 min 就会有 max,maxSize 定多少,官方没有默认值。maxSize 就是为了减小文件体积设置的一个上限,但是有些情况下,可能会违反 maxSize 的规则。比如某一个模块大于 maxSize 或者拆分不符合 minSize 时。
在项目中每一个按需加载的文件都是一个 chunk。包括 import ... from ...和 import()
对应于 maxSize,splitChunks 还有 maxAsyncSize,maxInitialSize。这里的 maxAsyncSize 只对异步导入模块起作用,maxInitialSize 只对入口文件起作用。
webpack5 已经对 splitChunks 做了最优配置。通常情况下,我们不需要做特殊处理只需要将 splitChunks 设置为 all 就可以了。
第二招 小文件聚合
所谓小文件聚合,就是将多个体积较小的 chunk 输出到同一个文件之中,这样做是为了尽量减少同一时间请求资源的数量。在上文中已经说过,浏览器是可以有 4 ~ 8 个并发的。也就是说它可以同时请求 4 ~ 8 个静态资源文件。如果同一时间请求的文件数量过多,就会出现排队现象,这样就会大大降低页面加载性能。
所以,当项目中小文件过多时需要适当的聚合成一个大文件,减少静态资源的并发数。
这里的小文件分为两种情况,一种是 js 或 css 文件,一种是图片资源。
组件合并
对于 js 或 css 资源,一般都是构成组件的资源,这种我们也可以通过 splitChunks 来进行配置。之前讲过 splitChunks 配置中的 maxSize 和 minSize。minSize 的意思就是控制一个文件最少多少体积(webpack 官方默认 20KB)。
图片聚合
long long ago,我们说到图片优化,都会提到一个词 “精灵图”,就是将多个小图片聚合到一张大图上,然后在使用的时候通过 background 的 positon 来进行定位。但是现在我们有了更好的工具。
1. SVG 文件
对于图标 svg,我们可以使用 iconfont。最简单的方法就是通过iconfont 网站 上传图标,自动生成类似下面的代码
!(function (d) {
var e,
s =
'<svg><symbol id="fcmd-error" viewBox="0 0 1024 1024"><path d="M512 1017.279c-279.061 0-505.282-226.223-505.282-505.282s226.221-505.279 505.282-505.279c279.059 0 505.282 226.223 505.282 505.282s-226.223 505.279-505.282 505.279zM512 69.878c-244.169 0-442.122 197.949-442.122 442.122s197.952 442.119 442.122 442.119c244.169 0 442.119-197.949 442.119-442.119s-197.949-442.122-442.119-442.122zM556.841 512.164l133.803 133.827c12.337 12.316 12.337 32.32 0 44.653-12.333 12.337-32.337 12.337-44.653 0l-133.827-133.803-134.708 134.708c-12.439 12.437-32.587 12.437-45.005 0-12.439-12.437-12.439-32.587 0-45.007l134.708-134.727-133.806-133.806c-12.335-12.335-12.335-32.32 0-44.657 12.337-12.333 32.32-12.333 44.657 0l133.806 133.806 135.716-135.716c12.437-12.419 32.587-12.419 45.007 0 12.437 12.437 12.437 32.587 0 45.026l-135.7 135.698z" ></path></symbol><symbol id="fcmd-question" viewBox="0 0 1024 1024"><path d="M874.016 874.048C674.4 1073.664 349.6 1073.664 149.952 874.048-49.664 674.432-49.664 349.568 149.952 149.952 349.6-49.664 674.4-49.664 874.016 149.952 1073.664 349.568 1073.664 674.432 874.016 874.048ZM828.768 195.232C654.08 20.544 369.888 20.576 195.232 195.232 20.512 369.92 20.544 654.112 195.232 828.8 369.888 1003.456 654.08 1003.488 828.768 828.8 1003.424 654.144 1003.456 369.888 828.768 195.232ZM588.8 540.032 564.064 564.8C558.016 570.816 553.312 577.888 550.048 585.728 546.4 594.56 545.12 600.352 545.12 608L481.12 608C481.12 591.776 484.064 577.792 490.976 561.248 497.408 545.6 506.784 531.552 518.816 519.552L543.52 494.816C554.048 484.256 562.304 471.936 568 458.176 573.6 444.672 576.48 430.432 576.608 415.84 576.256 380.512 547.392 351.904 512 351.904 476.384 351.904 447.392 380.864 447.392 416.48L447.392 459.808 383.392 459.808 383.392 416.48C383.392 345.568 441.088 287.904 512 287.904 582.4 287.904 639.776 344.768 640.608 414.976 640.608 415.264 640.608 415.52 640.608 415.808 640.608 416 640.608 416.256 640.608 416.48 640.416 439.264 635.872 461.504 627.136 482.656 618.208 504.192 605.312 523.52 588.8 540.032ZM544 736 480 736 480 672.672 544 672.672 544 736Z" ></path></symbol><symbol id="fcmd-success" viewBox="0 0 1024 1024"><path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m0 947.2c-240.64 0-435.2-194.56-435.2-435.2S271.36 76.8 512 76.8s435.2 194.56 435.2 435.2-194.56 435.2-435.2 435.2z m266.24-578.56c0 10.24-5.12 20.48-10.24 25.6l-286.72 286.72c-5.12 5.12-15.36 10.24-25.6 10.24s-20.48-5.12-25.6-10.24l-163.84-163.84c-15.36-5.12-20.48-15.36-20.48-25.6 0-20.48 15.36-40.96 40.96-40.96 10.24 5.12 20.48 10.24 25.6 15.36l138.24 138.24 261.12-261.12c5.12-5.12 15.36-10.24 25.6-10.24 20.48-5.12 40.96 15.36 40.96 35.84z" ></path></symbol><symbol id="fcmd-info" viewBox="0 0 1024 1024"><path d="M512 0C229.376 0 0 229.376 0 512s229.376 512 512 512 512-229.376 512-512S794.624 0 512 0z m0 955.904c-244.736 0-443.904-199.168-443.904-443.904S267.264 68.096 512 68.096s443.904 199.168 443.904 443.904-199.168 443.904-443.904 443.904z m-34.304-368.64V302.08a34.304 34.304 0 0 1 68.608 0v285.184a34.304 34.304 0 0 1-68.608 0z m78.848 146.432c0 24.576-19.968 44.544-44.544 44.544s-44.544-19.968-44.544-44.544c0-24.576 19.968-44.544 44.544-44.544s44.544 19.968 44.544 44.544z" ></path></symbol></svg>',
t = (e = document.getElementsByTagName("script"))[
e.length - 1
].getAttribute("data-injectcss");
if (t && !d.__iconfont__svg__cssinject__) {
d.__iconfont__svg__cssinject__ = !0;
try {
document.write(
"<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>"
);
} catch (e) {
console && console.log(e);
}
}
!(function (e) {
if (document.addEventListener)
if (~["complete", "loaded", "interactive"].indexOf(document.readyState))
setTimeout(e, 0);
else {
var t = function () {
document.removeEventListener("DOMContentLoaded", t, !1), e();
};
document.addEventListener("DOMContentLoaded", t, !1);
}
else
document.attachEvent &&
((o = e),
(c = d.document),
(i = !1),
(s = function () {
try {
c.documentElement.doScroll("left");
} catch (e) {
return void setTimeout(s, 50);
}
n();
})(),
(c.onreadystatechange = function () {
"complete" == c.readyState && ((c.onreadystatechange = null), n());
}));
function n() {
i || ((i = !0), o());
}
var o, c, i, s;
})(function () {
var e, t, n, o, c, i;
((e = document.createElement("div")).innerHTML = s),
(s = null),
(t = e.getElementsByTagName("svg")[0]) &&
(t.setAttribute("aria-hidden", "true"),
(t.style.position = "absolute"),
(t.style.width = 0),
(t.style.height = 0),
(t.style.overflow = "hidden"),
(n = t),
(o = document.body).firstChild
? ((c = n), (i = o.firstChild).parentNode.insertBefore(c, i))
: o.appendChild(n));
});
})(window);
在项目中直接引用这个 js 文件会在 body 中生成一个隐藏的 svg 标签,里面会包含多个 symbol 标签。以下就是自动生成的 svg 标签
<svg
aria-hidden="true"
style="position: absolute; width: 0px; height: 0px; overflow: hidden;"
>
<symbol id="fcmd-error" viewBox="0 0 1024 1024">
<path
d="M512 1017.279c-279.061 0-505.282-226.223-505.282-505.282s226.221-505.279 505.282-505.279c279.059 0 505.282 226.223 505.282 505.282s-226.223 505.279-505.282 505.279zM512 69.878c-244.169 0-442.122 197.949-442.122 442.122s197.952 442.119 442.122 442.119c244.169 0 442.119-197.949 442.119-442.119s-197.949-442.122-442.119-442.122zM556.841 512.164l133.803 133.827c12.337 12.316 12.337 32.32 0 44.653-12.333 12.337-32.337 12.337-44.653 0l-133.827-133.803-134.708 134.708c-12.439 12.437-32.587 12.437-45.005 0-12.439-12.437-12.439-32.587 0-45.007l134.708-134.727-133.806-133.806c-12.335-12.335-12.335-32.32 0-44.657 12.337-12.333 32.32-12.333 44.657 0l133.806 133.806 135.716-135.716c12.437-12.419 32.587-12.419 45.007 0 12.437 12.437 12.437 32.587 0 45.026l-135.7 135.698z"
></path>
</symbol>
<symbol id="fcmd-question" viewBox="0 0 1024 1024">
<path
d="M874.016 874.048C674.4 1073.664 349.6 1073.664 149.952 874.048-49.664 674.432-49.664 349.568 149.952 149.952 349.6-49.664 674.4-49.664 874.016 149.952 1073.664 349.568 1073.664 674.432 874.016 874.048ZM828.768 195.232C654.08 20.544 369.888 20.576 195.232 195.232 20.512 369.92 20.544 654.112 195.232 828.8 369.888 1003.456 654.08 1003.488 828.768 828.8 1003.424 654.144 1003.456 369.888 828.768 195.232ZM588.8 540.032 564.064 564.8C558.016 570.816 553.312 577.888 550.048 585.728 546.4 594.56 545.12 600.352 545.12 608L481.12 608C481.12 591.776 484.064 577.792 490.976 561.248 497.408 545.6 506.784 531.552 518.816 519.552L543.52 494.816C554.048 484.256 562.304 471.936 568 458.176 573.6 444.672 576.48 430.432 576.608 415.84 576.256 380.512 547.392 351.904 512 351.904 476.384 351.904 447.392 380.864 447.392 416.48L447.392 459.808 383.392 459.808 383.392 416.48C383.392 345.568 441.088 287.904 512 287.904 582.4 287.904 639.776 344.768 640.608 414.976 640.608 415.264 640.608 415.52 640.608 415.808 640.608 416 640.608 416.256 640.608 416.48 640.416 439.264 635.872 461.504 627.136 482.656 618.208 504.192 605.312 523.52 588.8 540.032ZM544 736 480 736 480 672.672 544 672.672 544 736Z"
></path>
</symbol>
<symbol id="fcmd-success" viewBox="0 0 1024 1024">
<path
d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m0 947.2c-240.64 0-435.2-194.56-435.2-435.2S271.36 76.8 512 76.8s435.2 194.56 435.2 435.2-194.56 435.2-435.2 435.2z m266.24-578.56c0 10.24-5.12 20.48-10.24 25.6l-286.72 286.72c-5.12 5.12-15.36 10.24-25.6 10.24s-20.48-5.12-25.6-10.24l-163.84-163.84c-15.36-5.12-20.48-15.36-20.48-25.6 0-20.48 15.36-40.96 40.96-40.96 10.24 5.12 20.48 10.24 25.6 15.36l138.24 138.24 261.12-261.12c5.12-5.12 15.36-10.24 25.6-10.24 20.48-5.12 40.96 15.36 40.96 35.84z"
></path>
</symbol>
<symbol id="fcmd-info" viewBox="0 0 1024 1024">
<path
d="M512 0C229.376 0 0 229.376 0 512s229.376 512 512 512 512-229.376 512-512S794.624 0 512 0z m0 955.904c-244.736 0-443.904-199.168-443.904-443.904S267.264 68.096 512 68.096s443.904 199.168 443.904 443.904-199.168 443.904-443.904 443.904z m-34.304-368.64V302.08a34.304 34.304 0 0 1 68.608 0v285.184a34.304 34.304 0 0 1-68.608 0z m78.848 146.432c0 24.576-19.968 44.544-44.544 44.544s-44.544-19.968-44.544-44.544c0-24.576 19.968-44.544 44.544-44.544s44.544 19.968 44.544 44.544z"
></path>
</symbol>
</svg>
这样 symbol 标签里面的内容就可以通过 use 在其他位置被引用。我们可以在代码中通过如下方式使用 svg:
<svg aria-hidden="true">
<use xlinkHref="#fcmd-info" />
</svg>
这样做的好处是,我们不需要引入单个的 svg 文件,而是引入了一个 svg 聚合文件。因为一个 svg 文件就只有几十 b,一个一个引入会造过多的 http 耗时。而将多个小的零散的 svg 聚合成一个稍大的文件,将大大减少 http 的资源浪费。
如果不想将图标托管在 iconfont.cn 上,也可以自己构建 iconfont 文件。
2. 其他图片格式
对于 svg 我们可以通过 iconfont 的方式来使用,对于其他格式的图片该怎么做呢?前文提过 精灵图是一种方法。还有另一种方法却是和聚合相反,而是将图片直接转换成 base64 格式的字符串,打包 import 图片的文件中,这样就不用再额外花销一个 http 去请求这个图片的资源了。 比如:
import xxPng from "../xx.png";
最后打包出来:
const xxPng = "data:image/png;base64,xxxxxx...";
这个需要在 webpack 中配置 url-loader。参考webpack 官网 url-loader
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: "url-loader",
options: {
limit: 8192,
},
},
],
},
],
},
};
但是,并非所有的图片都适合转成 base64。一般来说 png 图片转换成 base64 体积会增大 30%左右。也就是 10kb 的图片会变成 13kb 左右。
如果这个图片又是反复被引用的那么就会有重复的 base64 字符串打包到输出文件中,增加了文件体积。所以一般来说我们会限制转成 base64 的图片的大小不超过 10kb。上文中 webpack 官方给出的 demo 中 limit 是 8kb。
如果有重复使用的小图片我们也可以手动将图片转成 base64,做成模块:
// pngModule.js
export const png1 = "data:image/png;base64,xxxxxx...";
export const png2 = "data:image/png;base64,xxxxxx...";
export const png3 = "data:image/png;base64,xxxxxx...";
在使用时:
import { png1 } from "../pngModule.js";
这样做的好处就是将多个小图片聚合到一个模块文件中,使用 import {} form 方式又方便 webpack 进行 treeShaking,最大程度的减少了重复引入的问题。
当然以上方法也是在尽量压缩图片尺寸的基础上的。
3. CSS 图标绘制
能用 css 处理的图形,尽量不要用图片。
现在 css3 已经很强大了,能够做出很多令人惊叹的图像。
如下图:
其核心 css 代码压缩后只有 2kb,如果是使用图片的话大概率会超过 10kb。
第三招 异步加载
以上两招都是对文件体积出招,目的在于让资源加载时,尽可能的减少资源的浪费,提升页面加载性能。他们都是内功,是看不着的。
而第三招则是大招,合理的异步加载策略能让你的分包更好的体现出效果。
异步路由
这里最重要的一个函数,就是上文提到过的 import(),比如我们常说的异步路由,不管是 react-router 还是 vue-router,他们的路由异步加载都是需要通过 import()进行分包的。比如:
// AsyncComponent.jsx
import React, { Component } from "react";
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
// router.jsx
import React from "react";
import ReactDOM from "react-dom";
// 动态加载组件,用作代码分割
import asyncComponent from "./AsyncComponent";
import { Route, Switch } from "react-router-dom";
import "./index.css";
// 核心代码
const Example = asyncComponent(() => import("./Example"));
ReactDOM.render(
<Router>
<Switch>
<Route path="/example" component={Example} />
</Switch>
</Router>,
document.getElementById("root")
);
其中
const Example = asyncComponent(() => import("./Example"));
是核心代码,通过() => import()函数来异步引入 Example 组件。只有当路由切换时,才会去加载对应的组件,才会加载对应组件的文件,而不需要加载所有的文件。可以大大提升页面加载性能。
另外,对于一些逻辑比较复杂,体积比较大的组件,我们也可以通过异步导入的方式来使用。其方法和上文中异步路由类似,核心方法也是使用() => import()。
// AsyncComponent.jsx 这里改写成functino component 和上文中的 class component是一样的
import React, { useState, useEffect } from "react";
export default function asyncComponent(importComponent) {
return function AsyncComponent(props) {
const [Comp, setComp] = useState();
const initComponent = async () => {
const { default: component } = await importComponent();
};
useEffect(() => {
initComponent();
}, []);
return Comp ? <Comp {...props} /> : null;
};
}
// router.jsx
import React from "react";
import ReactDOM from "react-dom";
// 动态加载组件,用作代码分割
import asyncComponent from "./AsyncComponent";
import { Route, Switch } from "react-router-dom";
import "./index.css";
// 核心代码
const Example = asyncComponent(() => import("./Example"));
const App = () => {
return <Example />;
};
ReactDOM.render(<App />, document.getElementById("root"));
动态加载脚本
看上去 import()函数确实是异步加载的利器,但是它也有弊端。因为 import()函数是 webpack 实现的,它只能加载当前项目中,在 wepback 打包时打成 chunk 的模块。如果有第三方库函数库,想通过异步的方式引入 CDN 资源就无法实现了。
现在常用的一些第三方库都已经可以通过 npm 来引入项目,在项目打包时,我们可以通过 splitChunk 打成 vendor。但是这个 vendor 是同步代码,不是异步的。也就是首页渲染时,必然会被引入,而不是异步引入的。而且,有时候我们也会使用 CDN 做静态资源加速,那么这些静态资源就不能通过 import()来引入了。
下面我给大家介绍一个我自己写的私货 @rasir/script-loader。这是一个可以实现动态脚本加载的工具。
- 动态脚本加载工具
- 在项目中,有时候我们需要引入一些较大的 JS 库,如果直接通过打包到组件中,那么这个组件的文件尺寸就会很大。所以,我们可以在需要使用这些 JS 库的时候动态的加载相应的 min.js。就可以为组件文件节省很大的空间,同时也能提升打包速度。如果配合 CDN 使用,对前端性能提升会有很大提升。
- 主要原理是动态加载 script 标签或者 link 标签
这个工具大多数 api 都是返回 Promise 函数,这样你可以在 js 完全加载好了之后从 window 中获取到对应的对象。
而且支持链式加载。比如在引入 swipper.js 之前需要先引入 jquery.js。有了链式加载这样就可以保证多个 js 库如果有依赖关系的时候不会产生异常了。
导入的文件支持相对路径,也支持绝对路径。
具体使用方法,可以去 @rasir/script-loader 查看。
做这个工具的初衷,是因为我们的项目做成微服务后,有些第三方库,比如 antv/g6,echarts 等多个子项目都会打包到各自的 chunk 中,加重了页面加载的负担。
使用异步加载,只需要将这些库的 min.js 放到主应用的 public 中就可以不用重复打包了。再配和上合理的浏览器缓存策略,页面加载这些组件时性能会大大提升。
总结
以上三招只是前端性能优化的冰山一角。从 ngnix 配置到浏览器缓存,再到 http 请求再到静态资源加载,CDN 等,然后再到运行时,异常处理等等。前端性能优化能着手的地方很多,以上三招都只能算常规操作。
如果你是用 webpack 做工程化,合理使用以上三招绝对可以让你的页面性能提升 60%以上。
欢迎大家留言讨论。