效果对比
2、性能指标
优化之前:
优化之后:
页面渲染流程
哪些原因导致页面首屏时间慢?
- 首屏渲染所需要的js体积太大
- 接口耗时太长(串行调用时间大约为1.2s)
- 长页面,大概有10屏,渲染时间过长
优化手段
减小静态资源体积
1、webpack分包
使用webpack-bundle-analyzer分析依赖的结构 + webpack splitChunks配置自定义分包,将单个js的体积控制在100k左右。
通过观察,在http2的加持下,将js的体积控制在100kb以下的效果是比较好的。
下面也贴一下nextjs中客户端webpack的配置 (超出160kB则进行拆分)
const frameworkCacheGroup = {
chunks: "all",
name: "framework",
// Ensures the framework chunk is not created for App Router.
layer: _utils.isWebpackDefaultLayer,
test (module1) {
const resource = module1.nameForCondition == null ? void 0 : module1.nameForCondition.call(module1);
return resource ? topLevelFrameworkPaths.some((pkgPath)=>resource.startsWith(pkgPath)) : false;
},
priority: 40,
// Don't let webpack eliminate this chunk (prevents this chunk from
// becoming a part of the commons chunk)
enforce: true
};
const libCacheGroup = {
test (module1) {
var _module_type;
return !((_module_type = module1.type) == null ? void 0 : _module_type.startsWith("css")) && module1.size() > 160000 && /node_modules[/\\]/.test(module1.nameForCondition() || "");
},
name (module1) {
const hash = _crypto.default.createHash("sha1");
if (isModuleCSS(module1)) {
module1.updateHash(hash);
} else {
if (!module1.libIdent) {
throw new Error(`Encountered unknown module type: ${module1.type}. Please open an issue.`);
}
hash.update(module1.libIdent({
context: dir
}));
}
// Ensures the name of the chunk is not the same between two modules in different layers
// E.g. if you import 'button-library' in App Router and Pages Router we don't want these to be bundled in the same chunk
// as they're never used on the same page.
if (module1.layer) {
hash.update(module1.layer);
}
return hash.digest("hex").substring(0, 8);
},
priority: 30,
minChunks: 1,
reuseExistingChunk: true
};
// client chunking
return {
// Keep main and _app chunks unsplitted in webpack 5
// as we don't need a separate vendor chunk from that
// and all other chunk depend on them so there is no
// duplication that need to be pulled out.
chunks: (chunk)=>!/^(polyfills|main|pages\/_app)$/.test(chunk.name),
cacheGroups: {
framework: frameworkCacheGroup,
lib: libCacheGroup
},
maxInitialRequests: 25,
minSize: 20000
};
2、动态引入 & 按需加载
分包只能将单个大的js拆分为多个小的js,考虑到带宽的影响,还需要减少同时请求的js体积。
对于一些一开始不需要的js,可以使用动态加载的方式引入。
项目中对html2canvas进行了动态引入,(注意配合webpackPrefetch)
import(
/* webpackChunkName: "html2canvas", webpackPrefetch: true */
'html2canvas'
).then(async ({default: html2canvas}) => {
...
}, 'image/jpeg');
}).catch(() => {
Toast.show('生成报告图片失败,请点击重试');
});
3、其他静态资源的优化
-
图片
-
字体
- 注意font-display的使用
- fonttools字体集抽离 + unicode-range 实现字体集的按需加载
接口调用时机调整
因为是csr的项目,在react的组件中发出接口请求,需要等待js全部加载完成,才可以发出请求,然后接收响应数据进行渲染。
所以想到了将接口请求时机提前的方式。
开始我使用的方式是,在index.html中head标签内去编写内联的js,发送请求,然后将请求对应的promise挂载到windows上,在组件中,使用window上的promise来获取数据,大概的代码如下:
//挂载promise在window上
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>xxx</title>
<link rel="shortcut icon" href="/planning-report-fe/static/favicon.ico">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<script>
(() => {
window.submitQuestionnairePromise = new Promise((resolve, reject) => {
submitQuestionnaire()
.then(submitResStr => {
const submitRes = JSON.parse(submitResStr);
if (submitRes.code === 0 && submitRes.data) {
resolve(true);
}
else {
reject(submitRes);
}
})
.catch(e => {
reject(e);
});
});
window.submitQuestionnairePromise
.then(() => {
window.queryReportDataPromise = new Promise((resolve, reject) => {
queryReportData()
.then(reportDataResStr => {
const reportDataRes = JSON.parse(reportDataResStr);
if (reportDataRes.code === 0 && reportDataRes.data) {
resolve(reportDataRes.data);
}
})
.catch(e => {
reject(e);
});
});
})
.catch(e => {
console.error(e);
});
})();
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
//组件中获取数据
window.submitQuestionnairePromise?.then(submitRes => {
if (submitRes) {
...
window.queryReportDataPromise?.then(reportDataRes => {
...
}).catch(() => {
setIsShowErrorPage(true);
});
}
}).catch(() => {
setIsShowErrorPage(true);
});
但是在上线后,我发现出现了一个问题,第1个接口是在js请求之前就发出了,但是第2个请求,有时还是会等js全部加载和执行完之后才去请求。
经过分析,我发现是js的事件循环机制导致的。
解决方法:webworker将请求的线程独立于主线程之外。
分段渲染(先让首屏的内容渲染出来)
/**
* 页面上方的模块静态引入,下方的模块动态引入
*/
import {lazy} from 'react';
import TeacherNodeModule from './teacherNoteModule';
import HeaderModule from './headerModule';
import BaseInfoModule from './baseInfoModule';
import StudentDiagnosticModule from './studentDiagnosticModule';
const RecommondSubjectModule = lazy(() => import(
/* webpackChunkName: "recommondSubjectModule" */
'./recommondSubjectModule'
));
const ScorePositionModule = lazy(() => import(
/* webpackChunkName: "scorePositionModule" */
'./scorePositionModule'
));
const ImportScorePlanModule = lazy(() => import(
/* webpackChunkName: "importScorePlanModule" */
'./improveScorePlanModule'
));
const RecommondSchoolModule = lazy(() => import(
/* webpackChunkName: "recommondSchoolModule" */
'./recommondSchoolModule'
));
const ImprovementPlanModule = lazy(() => import(
/* webpackChunkName: "improvementPlanModule" */
'./improvementPlanModule'
));
const SubjectScoreTargetModule = lazy(() => import(
/* webpackChunkName: "subjectScoreTargetModule" */
'./subjectScoreTargetModule'
));
const SubjectImprovePlanModule = lazy(() => import(
/* webpackChunkName: "subjectImprovePlanModule" */
'./subjectImprovePlanModule'
));
const ExamAnalysisModule = lazy(() => import(
/* webpackChunkName: "examAnalysisModule" */
'./examAnalysisModule'
));
const IdealSchoolModule = lazy(() => import(
/* webpackChunkName: "idealSchoolModule" */
'./idealSchoolModule'
));
const EndModule = lazy(() => import(
/* webpackChunkName: "endModule" */
'./endModule'
));
const CalendarNotesModule = lazy(() => import(
/* webpackChunkName: "calendarNotesModule" */
'./calendarNotesModule'
));
export {
BaseInfoModule,
HeaderModule,
TeacherNodeModule,
StudentDiagnosticModule,
RecommondSubjectModule,
ScorePositionModule,
ImportScorePlanModule,
RecommondSchoolModule,
ImprovementPlanModule,
CalendarNotesModule,
SubjectScoreTargetModule,
SubjectImprovePlanModule,
ExamAnalysisModule,
IdealSchoolModule,
EndModule,
};