1. 写在前面
当我们基于vue-cli
搭建前端项目时,会发现在public目录下默认存在一个index.html
,并在之后的构建过程中会以该文件为页面模板将打包产物挂载到指定位置上,最终生成符合预期的产物内容。基于此能力,我们顺理成章地可以考虑将当前项目中的一些静态的、全局公共逻辑的代码提前写入index.html
中,而在构建阶段只负责动态产物的生成和挂载(参考 webpack/externals 配置规则)。这样可以有效提升每次打包构建时的效率,同时进一步保证静态脚本资源的稳定性。下述示例展示了将 vue.js 库、vant 组件库等进行提前挂载,并通过externals
配置项将上述资源排除在webpack
的构建过程。
// webpack.config.js
module.exports = {
// ...
externals: {
vue: "Vue",
axios: "axios",
vant: "vant",
lodash: "_",
},
}
// public/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
<link rel="stylesheet" href="https://unpkg.com/vant@2.12/lib/index.css" />
<script defer src="https://cdn.jsdelivr.net/npm/vue@2.7.14"></script>
<script defer src="https://unpkg.com/vant@2.12/lib/vant.min.js"></script>
<script defer src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script defer crossorigin src="//unpkg.com/lodash@4.17.21/lodash.min.js"></script>
</head>
<body>
<div id="app" />
</body>
</html>
之后在构建阶段通过html-webpack-plugin
插件配置好模板路径后,将打包产物以<link>
、<script>
标签形式挂载到模板上,并最终生成符合预期的html文件。
诚然,html-webpack-plugin
已经提供了一系列配置项(参见 html-webpack-plugin 配置详情),且上例中的静态构建能力在绝大多数情况下足够使用,但在某些特定场合下,作为开发人员还是希望最终的html能够再灵活可配置一些,以便做进一步的性能优化实践。
2. EJS 介绍及优化场景
2.1 EJS 简介及首屏优化背景
EJS(Embedded JavaScript Templates,下文统一称 ejs)是一种简单而灵活的模板引擎,用于将数据动态渲染到网页上(详情见 EJS中文官网,本文不做赘述)。基于该能力,便可以在构建阶段传递更为灵活的数据给到模板引擎,实现视图层与数据层的分离,最终生成符合预期的 html 结果。ejs总体学习成本不高,非常适合快速普及使用。
下面再简单聊聊前端性能优化大知识体系中的首屏加载优化。针对首屏加载方向的优化的手段较多,且每种方式的侧重点也不一样。在此不展开详细讨论及代码示例铺设。其中,如何减少构建产物中的主包体积,避免过多、过冗余的代码被打包进主包中去,是一种比较常见的策略,因为主包资源(相关js、css)的请求、加载和执行确实会非常影响首屏加载时间,造成糟糕的用户体验。因此,可以考虑在构建过程中引入 tree-sharking 或通过terser
分别将未引入的代码清除和代码压缩,这样也会有比较理想的效果。但在此基础上,如果我们能提前得知页面首屏资源是哪些,并在构建阶段通过生成动态的 html 文件实现当前业务场景下首屏资源的提前加载,这样的优化效果将会是很大的,而上述ejs就提供了这样的能力。
2.2 构建产物优化
针对构建产物中主包体积过大、代码过于冗余的问题,通常情可考虑从几个方面入手:
-
冗余代码清除。即对当前js、css进行瘦身,首先考虑的是进行基于 tree-sharking 的代码摇拽,将多余引入的依赖资源“摇”下来。之后通过检查代码,尽可能对第三方依赖包(工具包、组件库等)做按需引入,避免一次性全局引入造成包体积冗余;
-
构建产物压缩。通过借鉴
terser
做最后的代码压缩工作,把最终多余、重复的业务代码做全面去除和压缩处理; -
从主包中抽离其他业务代码。如果一个主包内容中有比较多的代码是通常绝大多数场景下并不会触发到的,那这部分代码我们就应该把它从主包中提取出来,并在需要的业务代码中做按需引入。所以通常情况下,可以按照此方式优化主包体积(
index.[hash].js
、index.[hash].css
),尽可能让主包只承载全局公共资源; -
对于从主包中抽离出来的其他模块代码,基本可以认为这部分内容不会是全局业务代码的核心模块,针对这部分资源的加载速度的快慢及阻塞并不会带来太大影响,但也不排除这部分内容有可能作为首屏内容去展示,低代码平台就会有这样的情况发生。
2.3 动态html构建
以低代码平台为例,这类产品的设计和诞生初衷是为能够快速响应业务诉求,通过简单的业务组件拖拽形式快速搭建出一个html页面来迅速投放到各大平台上而无需开发介入,从而第一时间抢占市场。在此背景下,运营在页面首位可以按照当前业务需求任意拖拽组件,因此只单纯优化主包体积其实并不能带来很明显的收益,需要思考如何让当前首位上的组件资源优先加载,进而优先渲染展示,提升整体响应速度。
图 参考某低代码平台,左侧为内容组件库(也称物料组件库)
针对低代码平台场景,通常情况下除非极个别常用组件的资源可以考虑打入主包内,其余组件都建议把产物单独打包出来,避免对主包造成过多耦合。因此使用 Webpack 的动态加载功能将除核心组件外的其他非核心组件模块更改为异步导入。
上述代码示意中将商品卡片、选项卡等组件设置为同步导入,这些组件对应的资源产物会包含入主包中,也就意味着通常情况下大多数的活动页面都会配置这些同步导入的组件,这样做的好处是可以有效减少页面加载过程中的资源请求数量,不失为一种优化策略。而其余组件,如文本、轮播、榜单等组件为异步导入,最终产物会单独打包生成。
现在假设当前活动页的主题为会员排行榜,并要求配置的第一个组件为RankingCard,这时就需要考虑如何能提前加载 RankingCard 组件的资源(src_components_ranking_card.[hash].js
、src_components_ranking_card.[hash].css
),如果当前项目架构允许,甚至可以将其提升到主包前就去请求加载。这种场景下,我们通过ejs
的动态生成指定html能力就可做到。
通过ejs
,我们可以在node服务构建阶段通过 npm-ejs 模块进行模板渲染,并将当前组件集数据导入。构建定制化页面。比如下述示例代码中,在生成新html页面的构建过程中就将组件名称集componentList
。
const path = require('path);
const ejs = require('ejs');
const htmlTemplate = path.join('@/public/template', 'index.html.ejs');
const htmlCode = ejs.renderFile(htmlTemplate, {
title: XXX
activityId: XXX,
componentList: XXX
})
const outputPath = path.resolve('output/id/html');
fs.writeFileSync(outputPath, htmlCode)
如果是基于 express 搭建的node环境,可以直接通过 app.set('view engine', 'ejs')
配置声明ejs,再通过res.render
实现相应html的动态生成。详情 参见github相关示例。
var express = require('express');
var app = express();
app.set('view engine', 'ejs'); // 声明设置ex[ress使用的模板引擎为ejs
app.set('views', __dirname + '/template'); // 设定模板文件 *.html.ejs 的存放位置
// index page
app.get('/', function(req, res) {
res.render('pages/index', {
mascots: mascots,
tagline: tagline
});
});
app.listen(8080);
index.html.ejs
模板中获取到传入的相应数据(如上例中的title
、activityId
、componentList
等),通过 componentList 判定当前活动页的组件配置顺序。比如该场景中,我们封装一个函数用来过滤出非主包内,且渲染顺序要求优先的组件集(preComponentList
),并将其提前渲染
同样,也可以基于该方法提前加载相应组件的css脚本。
3. 总结
综上所述,通过 ejs 提供的动态模板渲染机制可以在某些特定场合下有效提升页面首屏资源加载时机,进而提升首屏性能,带来更好的用户体验。当我们试图从构建过程中努力挤牙缝似的去清理冗余代码、极致压缩资源体积时,不妨尝试一下这种直接改变资源加载顺序的动态手段。