您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~
前言
最近在做公司几个项目性能优化,整理出一些比较有用且常见的case来分享一下。
A项目优化
白屏相关
DNS预连接、资源预解析
对于公共域名g.alicdn.cmon,DNS预请求:
html
<link rel="preconnect" href="//g.alicdn.com" crossorigin />
<link rel="dns-prefetch" href="//g.alicdn.com" />
对于一些资源,资源预加载:
html
<link rel="preload" href="https://g.alicdn.com/eleme-risk/chuangdao-pc/0.0.99/js/index.js" as="script" />
<link rel="preload" href="//g.alicdn.com/alilog/mlog/aplus_v2.js" as="script" />
结果:白屏时间减少400~600ms左右。
页面级路由懒加载
原本创道打包出来的JS文件只有一个bundle.js,涵盖了整个项目的业务代码,对于城市CM来说,可能访问最多的就是新增定向看和任务详情两个页面,所以对于首屏加载是不友好的,应该优化成访问哪个页面加载对应页面的资源,基于Ice2.0调研,将路由中的组件都转换为懒加载模式:
- routes.ts
typeScript
import { lazy, IRouterConfig } from 'ice';
// ice不支持layout组件设置为懒加载
import Layout from '@/layouts/BasicLayout';
const Home = lazy(() => import(/* webpackChunkName: 'Home' */ '@/pages/Home'));
const NotFound = lazy(() => import(/* webpackChunkName: 'NotFound' */ '@/components/NotFound'));
const ManualDetect = lazy(() => import(/* webpackChunkName: 'ManualDetect' */ '@/pages/ManualDetect'));
const AddMission = lazy(() => import(/* webpackChunkName: 'addMission' */ '@/pages/ReconnaissanceMission/add-mission'));
const MissionDetail = lazy(
() => import(/* webpackChunkName: 'missionDetail' */ '@/pages/ReconnaissanceMission/missionDetail'),
);
const NewMissionDetail = lazy(
() => import(/* webpackChunkName: 'newMissionDetail' */ '@/pages/ReconnaissanceMission/newMissionDetail'),
);
const NoPermission = lazy(() => import(/* webpackChunkName: 'NoPermission' */ '@/pages/NoPermission'));
const Board = lazy(() => import(/* webpackChunkName: 'Board' */ '@/pages/Board'));
const BusinessInsight = lazy(() => import(/* webpackChunkName: 'BusinessInsight' */ '@/pages/BusinessInsight'));
const ChuangDaoInsight = lazy(() => import(/* webpackChunkName: 'ChuangDaoInsight' */ '@/pages/ChuangDaoInsight'));
const Report = lazy(() => import(/* webpackChunkName: 'Report' */ '@/pages/Report'));
const routes: IRouterConfig[] = [
{
path: '/',
component: Layout,
children: [
{
path: '/manualDetect',
component: ManualDetect,
},
{
path: '/addMission',
component: AddMission,
},
{
path: '/MissionDetail',
component: MissionDetail,
},
{
path: '/newMissionDetail',
component: NewMissionDetail,
},
{
path: '/',
exact: true,
component: Home,
},
{
path: '/noPermission',
exact: true,
component: NoPermission,
},
{
path: '/board',
exact: true,
component: Board,
},
{
path: '/businessInsight',
exact: true,
component: BusinessInsight,
},
{
path: '/chuangDaoInsight',
exact: true,
component: ChuangDaoInsight,
},
{
path: '/report',
exact: true,
component: Report,
},
{
component: NotFound,
},
],
},
];
export default routes;
2.build.json
typeScript
{
// ...
"router": {
"lazy": true
}
}
线上效果:
首屏在A页面:
只请求了对应A页面的代码,JS文件大小12.7KB,再进入到立即检查页面:
继续请求了对应跳转新页面的代码,文件大小也是KB量级的,再看一下优化前的首屏请求情况,无论访问哪个页面,请求的资源是一样的。
结果:白屏时间整体降低,请求资源大小整体下降。
构建相关
优化本地热更新时间
创道的本地热更新时间比较慢,大约在8~9秒,基于ice运行时中间件在每次代码变更时加入缓存同时移除对node_module目录下的babel转换。
typeScript
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.module
.rule('tsx')
.test(/.jsx?|.tsx?$/)
.exclude.add(/node_modules/)
.end()
.use('babel-loader')
.tap((options) => {
return {
...options,
cacheDirectory: true,
};
});
});
};
在build.json中注入该插件:
typeScript
{
// ...
"plugins": [
"@ali/build-plugin-faas",
[
"build-plugin-ignore-style",
{
"libraryName": "antd"
}
],
"@ali/build-plugin-ice-def",
"./src/index.ts"
]
}
结果:热更新时间降低到4秒左右,降低50%。
构建包大小优化
CDN资源替代项目依赖包
通过webpack可视化工具可以看到创道PC端的一些依赖包体积偏大,影响了页面渲染的时间:
从上图可以看到:在开发环境整个构建包体积达到了19.44MB,echarts、antv、moment这些包,体积都比较大,达到了MB量级,并且在项目中前两者使用频率很低,只有引用过一次,对于这种情况,考虑将依赖包转换为CDN引入的方式,原因如下:
- 减少打包产物大小;
- 减少白屏时间;
- 版本固定,使用频率低,通过CDN单独引入还会有浏览器强缓存的效益;
解决方案:
通过webpack中externals,解绑对于node_modules中枚举包的编译,并且在项目index.html中从CDN引入所列举到的包。
typeScript
{
// ...
"externals": {
"echarts": "echarts",
"moment": "moment"
},
}
这里的key,value值分别对应npm中的包名和CDN引入后在window下的全局变量名,找包的CDN路径很简单,但是如何知道全局变量名是什么呢?
可以打开CDN链接,格式化代码,大概是这个样子的:
typeScript
function(e, t) {
"object" == typeof exports && "object" == typeof module ? //判断环境是否支持commonjs模块规范
module.exports = t(require("vue")) :
"function" == typeof define && define.amd ? //判断环境是否支持AMD模块规范
define("ELEMENT", ["vue"], t) :
"object" == typeof exports ? //判断环境是否支持CMD模块规范
exports.ELEMENT = t(require("vue")) :
e.ELEMENT = t(e.Vue)
} ("undefined" != typeof self ? self: this,function(e){
//省略...
});
只需要看一下立即执行函数向外暴露的变量名是什么即可。
代码分割
对于项目中多次引用到的包和公共模块,开启webpack代码分割模式,这部分代码写在之前定义的运行时中间件中:
typeScript
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.optimization.splitChunks({
cacheGroups: {
vendor: {
priority: 1,
test: /node_modules/,
chunks: 'initial',
minChunks: 1,
minSize: 0,
name: 'vendor',
filename: 'vendor.js',
},
common: {
chunks: 'initial',
name: 'common',
minSize: 100,
minChunks: 3,
filename: 'common.js',
},
},
});
});
};
抽离出来的vendor.js模块如图:
结果:优化后的构建包体积为9.1MB,降低了50%以上大小。
目前对于创道H5做了如下优化内容:
B项目优化
白屏相关
HTML文件脚本加载改造
由于JS是单线程,脚本加载会直接阻塞页面渲染,因此对于一些直接放在HTML模板中并且优先级较低的JS文件,在创道H5中例如aplus埋点、vconsole判断加载包、exlog性能监控等与用户体感上无关的脚本文件直接异步加载解析即可:
html
<script defer>
try {
const isXuanYuan = /AliApp(EVE//i.test(navigator.userAgent);
if (isXuanYuan) {
document.documentElement.setAttribute('data-theme', 'xy')
} else {
document.documentElement.setAttribute('data-theme', 'default')
}
} catch (e) {
}
</script>
<script defer>
(function (w, d, s, q, i) {
w[q] = w[q] || [];
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s);
j.async = true;
j.id = 'beacon-aplus';
j.setAttribute('exparams', 'clog=o&aplus&sidx=aplusSidx&ckx=aplusCkx');
j.src = 'https://g.alicdn.com/alilog/mlog/aplus_wap.js';
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'aplus_queue');
</script>
<script defer>
const domain = location.hostname;
let env = 'prod';
if (/.(((alibaba|taobao|tmall).net)|daily.elenet.me)$/.test(domain) || location.hostname === 'local.ele.me') {
// 集团&饿了么 DAILY
env = 'dev';
} else if (/^(pre|ppe)-\w+./.test(domain)) {
// 集团&饿了么 PRE
env = 'pre';
}
console.log('env:', env)
const debug = {
dev: true,
pre: false,
prod: false,
} [env];
window._ex = {
biz: 'a2fe9.26877649', // 配置spm或业务标识
bizType: 'KOUBEI', // 固定入参,不要更改
debug: debug, // 开启后会在console打印日志,但注意不会上报,仅用于调试
enableOutsidePerformance: true, // 端外上报性能需要开启
commonParams: {}, // 添加全局参数,也可以直接赋值到window.ExLog.commonParams = {}
whiteScreen: true, // 是否开启白屏监控
};
// if (env !== 'prod') {
// eruda.init();
// }
if (env !== 'prod') {
const vConsoleScript = document.createElement('script');
vConsoleScript.src = 'https://cdn.bootcdn.net/ajax/libs/vConsole/3.9.1/vconsole.min.js';
document.body.appendChild(vConsoleScript);
vConsoleScript.onload = () => {
var vConsole = new VConsole();
}
}
</script>
<script src="https://gw.alipayobjects.com/as/g/koubei-data-center/exlog/1.4.1/index.js" defer></script>
优化前:
资源加载的并发性偏低,也直接影响到了云鼎接口的调用时机,平均在1400ms的时候才会调(走到useEffect),LCP平均为1300ms。
优化后:
资源加载的并发度提高了很多,并且平均在1100ms的时候就会开始调云鼎,LCP平均为1000ms,提升了300ms。
DNS预请求、资源预解析
由于项目使用umi,开发创道本身umi plugin给head插入一些link标签从而进行优化:
/plugins/preloadPlugin.ts
typeScript
import type { IApi } from 'umi';
export default (api: IApi) => {
api.addHTMLLinks(() => {
return [
{
href: '//shadow.elemecdn.com',
rel: 'dns-prefetch',
},
{
href: '//g.alicdn.com',
rel: 'dns-prefetch',
},
{
href: '//gw.alipayobjects.com',
rel: 'dns-prefetch',
},
{
href: '//render.alipay.com',
rel: 'dns-prefetch',
},
{
href: 'https://shadow.elemecdn.com/faas/chuangdao-h5-fe-gray/umi.c166c725.js',
rel: 'preload',
as: 'script',
},
{
href: 'https://shadow.elemecdn.com/faas/chuangdao-h5-fe-gray/layouts__BasicLayout.e2bc9944.async.js',
rel: 'preload',
as: 'script',
},
{
href: 'https://shadow.elemecdn.com/faas/chuangdao-h5-fe-gray/wrappers.b5ead63e.async.js',
rel: 'preload',
as: 'script',
},
];
});
};
.umirc.ts中加入该插件
typeScript
import { defineConfig } from 'umi';
export default defineConfig({
// ...
plugins: [require.resolve('./src/plugins/preloadPlugin.ts')],
});
优化前:
FP/FCP与LCP跨度较大,js资源请求比较分散
优化后:
LCP快了500ms左右,同时js资源请求并发度高了,重复利用起来了。
结尾
本文记录了博主工作中实际优化到的一些实用case,优化是灵活的,需要根据自己的场景来定,对你有帮助那就最好不过啦。
如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~