极致性能体验编码框架优化
在线课程:云课堂视频课程

我们说的“快”,并不仅仅指浏览器器加载页面快,就是常说的秒开率,一般指DomContentLoad时间。但是“快”其实包含更多的含义,除了前面说的浏览器加载快,还包含浏览器解析快(Javascript脚本发布时通常都会做代码压缩混淆,不仅是减少体积,也为了安全性),JS脚本编译快(我们知道javascript在浏览器的javascript虚拟机【managed runtime environment for JavaScript,JavaScript托管运行时环境】中运行的,所以也需要编辑JS脚本成字节码,才能运行),最后一个就是javascript执行快。
优化策略
链路优化中,我们已经解决了JavaScript下载加速的问题,那么剩下的优化工作主要集中在优化浏览器解析、编译并执行JS脚本。影响浏览器解析和执行JS脚本的因素主要是JS脚本的体积大小和代码的复杂程度。所以编程代码优化实践主要是减少代码的体积和按需降低代码复杂度,实现浏览器解析快,JS脚本编译快。
- 代码体积大,加载就会耗时,而且占用cdn存储资源和http请求资源,浏览器解析时暂用内存多,分析代码耗时。
- 代码复杂度高,代码解析就比较耗时。如果依赖一些复杂的类库,还要考虑库的解析和执行时间。浏览器解析代码会占用更多内存,使用堆栈更深,执行耗时。
首屏渲染

有许多方法可以用来减少程序的初始化加载时间。最小化加载的 JavaScript 数量:代码越少,解析耗时越少,运行时间越少。为了达到此目的,可以用特殊的方法传输必需的代码而不是一股劳地加载一大坨代码。比如,PRPL 模式即表示该种代码传输类型。或者,可以检查依赖然后查看是否有无用、冗余的依赖导致代码库的膨胀。然而,这些东西需要很大的篇幅来进行讨论。可以先参考下:提升javascript代码编译速度的几点建议。这里我们主要介绍PRPL模型。
移动网络非常慢。这些年,网络已从以文档为中心的平台演化为一流的应用平台。 有赖于平台本身的进步(例如服务工作线程)以及我们用于构建应用的工具和技术,用户在网络上几乎可以通过虚拟方式执行任何操作,就像在本机应用中操作一样。同时,我们大量的计算也已经发生变化,从使用快速、稳定网络连接的强大桌面设备转移到连接经常较慢、不稳定(或两者兼有)的相对欠强大的移动设备上。特别是在孕育着下一批十亿用户的地方,这一点体现得尤为真切。很遗憾,我们在桌面时代设计用于构建和部署强大、功能丰富的网络应用的模式通常会导致应用在移动设备上的加载时间过长 - 漫长的时间让很多用户选择放弃应用。这为创建新模式提供了机会,新模式需要利用现代网络平台功能更快速、更精细地提供移动网络体验。PRPL 就是这样一种模式。
PRPL 模式
PRPL 是一种用于结构化和提供 Progressive Web App (PWA) 的模式,该模式强调应用交付和启动的性能。 它代表:
- 推送 - 为初始网址路由推送关键资源。
- 渲染 - 渲染初始路由。
- 预缓存 - 预缓存剩余路由。
- 延迟加载 - 延迟加载并按需创建剩余路由。
除了针对 PWA 的基本目标和标准外,PRPL 还竭力在以下方面进行优化:
- 尽可能减少交互时间
- 特别是第一次使用(无论入口点在何处)
- 特别是在真实的移动设备上
- 尽可能提高缓存效率,特别是在发布更新时
- 开发和部署的简易性
PRPL 的灵感来源于一套现代网络平台功能,不必在首字母缩略词中打出每个字母或使用每个功能就可以应用这一模式。实际上,PRPL 更多的是一种思维模式和提高移动网络性能的长期愿景,而不仅仅是特定技术或技巧。PRPL 背后的理念并不新,但该方法由 Polymer 团队构建框架和命名,并在 Google I/O 2016 上公布。如果您的单页面应用 (SPA) 采用以下结构,PRPL 完全适用:
- 应用的主_进入点_从每个有效的路由提供。 此文件应非常小,它从不同网址提供,因此会被缓存多次。 进入点的所有资源网址都需要是绝对网址,因为它可以从非顶级网址提供。
- Shell 或 App Shell,包含顶级应用逻辑、路由器,等等。
- 延迟加载的应用_片段_。片段可以表示特定视图的代码,或可延迟加载的其他代码(例如,首次绘制不需要的部分主应用,如用户与应用交互前未显示的菜单)。Shell 负责在需要时动态导入片段。
服务器和服务工作线程协同为非活动路由预缓存资源。
用户切换路由时,应用会延迟加载尚未缓存的任何所需资源,并创建所需视图。 路由重复访问应当可以立即交互。 服务工作线程这时可以提供很大帮助。
下图显示了使用web components构建的一个简单应用的组件:

在此图表中,实线表示_静态依赖项_:使用 <link>
和 <script>
标记在文件中标识的外部资源。 虚线表示_动态_或_按需加载的依赖项_:根据 Shell 所需加载的文件。构建过程会构建一个包含所有这些依赖项的图表,服务器会使用此信息高效地提供文件。

这里是简单介绍了PRPL的思想,后续文章会介绍具体PRPL应用。
首屏渲染的含义主要是要开发者关注自己所开发页面的重点,能够有意识的区分出首屏关键内容和资源和非首屏内容。正如本系列课程开篇中提到,要求开发者首先要了解业务。

了解开发的重点,那么我们在开发中可以将首屏编码和非首屏分开,着重优化首屏模块,优先加载和执行,打包策略上也可以有所侧重和区分。待首屏渲染完成,再去加载其他资源,渲染用户不可见部分内容(可能是一个全屏弹窗,可能是一个二级页面,或者是首页的下半部分页面)。具体使用可以先参考这里:thinking-prpl
懒加载组件
LazyLoad懒加载组件是为了实现页面数据懒加载,主要是实现图片懒加载和页面模块懒加载。
- 图片懒加载,我们渲染页面模块时,通常是一个组件,如果是商品列表这种,组件最后渲染出来的html就包含了许多img标签,如果立马加载这些图片会占用浏览器资源。特别是在首屏渲染时,会影响首屏时间。如果图片很多,很大,通常我们会做图片占位,然后懒加载,如果图片不多,图片位置也不大,那么可以预判下是否影响首屏,可以不做懒加载。
- 模块懒加载,在用户交互操作中,判断模块是否在可是窗口内,如果在视口,那么就要渲染,当然通常是留有余量的,比如下拉滚动页面,需要在快到达视口的时候就初始化模块。
因为在手机端H5页面基本都会是下拉加载展示的形态,所以懒加载是必不可少的基础功能组件。我们可以采用H5最新的IntersectionObserver方法轻松实现判断元素是否进入视口,当然对于不支持这个API的浏览器依然可以使用getBoundingClientRect这个api拿到元素的位置信息进行计算作为退化方案。代码如下: github代码
注意:代码中监听了orientationchange事件,在手机横竖屏切换时,需要重新计算。
滚动加载分页组件
对于移动端页面,最常见的交互形式莫过于下拉长列表。常见的实现方式有两种:
- 基于Virtual DOM方式,基于数据驱动开发,像使用React、Vue等框架开发,通过追加数据实现长列表加载展示。这样的开发方式比较简单,但是问题多多。首先就是性能问题,每次要重新渲染整个DOM,这也是为什么一般React、Vue在检查列表循环的时候都要求为循环组件提供一个唯一的key,这样方便快速比对diff DOM更新。然后就是对于稍微复杂的交互处理起来很麻烦,比如多TAB切换时,做DOM回收和复用,就很难做到。
- 基于DOM操作方式,其实基于ES6模块和字符模板也是很好的开发方式。DOM的操作方式更灵活,但是对于这种下拉滚动加载的情况,需要我们统一处理下拉滚动事件。主要是判断container是否滚动到底部,如果滚动到底部,抛出事件回调。
对于滚动加载组件,比简单的方法是在container内最后面插入一个loading元素,然后判断这个loading元素是否进入视口,进入视口可以采用lazyload懒加载的判断逻辑。那么整体思路就清晰了,参考一下代码,github代码
注意:为了不影响性能,我们在监听scroll事件的时候最后都要加上passive: true这个参数,以免阻塞浏览器UI渲染。
web worker
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。详细介绍参考:web worker介绍
其实用起来很简单,我们先创建一个fetch.js,用于获取数据
### fetch.js
self.addEventListener(‘message’, e => {
let url = e.data;
fetch(url).then(res => {
if (res.ok) {
self.postMessage(res);
} else {
throw new Error(’error with server’);
}
}).catch(err => {
self.postMessage(err.message);
});
})
然后在页面上通过Worker函数注册worker,
let worker = new Worker('fetch.js');
// 发送消息
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});
// 接收消息
worker.onmessage = (e) => { // block statements }
// 或者
worker.addEventListener('message', (e) => { // block statements })
// handle Runtime errors
worker.addEventListener('error', (e) => { // block of statements })
// close worker
worker.terminate();
规范规定有3中类型的web workers:
dedicated workers
shared workers
service workers


Service worker 是事件驱动的worker,注册到某个特定的域名,特定路径。它可以控制它所注册的域名路径下的所有资源,可以拦截这些资源请求,可以用来做缓存。具体参考使用 Service Workers提升体验

task切分
其实在刚开始部分我们介绍RAIL模型的时候,我们介绍Idle这部分时,主要的思路是:
利用空闲时间完成推迟的工作。例如,尽可能减少预加载数据,以便您的应用快速加载,并利用空闲时间加载剩余数据。 推迟的工作应分成每个耗时约 50 毫秒的多个块。如果用户开始交互,优先级最高的事项是响应用户。

要实现小于 100 毫秒的响应,应用必须在每 50 毫秒内将控制权返回给主线程,这样应用就可以执行其像素管道、对用户输入作出反应,等等。以 50 毫秒块工作既可以完成任务,又能确保及时的响应。
对的,这里提到了task,就是任务快。前面的知识点中,我们也介绍了浏览器的事件模型(事件队列,micro task和macro task,忘记了看这里: [浏览器线程理解与microtask与macrotask]12),浏览器主线程阻塞超过100ms就会给用户造成卡顿的感觉,事件队列执行的时候是放回到主线程执行的,为了防止浏览器阻塞,每个task任务执行时间就不能超过50ms,否则就不能保证在最坏的情况下,100ms内把控制权交回到主线程,让主线程处理其他事情。
其实也没有想象中的那么难,思路就是基于事件队列来拆分task,其实也就是异步处理。我们有两种方法:
- 基于定时器setTimeout,setInterval等(对应于Macro Task,执行优先级低)
- 基于Promise(对应于Micro Task,执行优先级高)
所以呢,我们可以封装自己的代码块,将非必要立即执行的任务放在这些task任务里,如果是很复杂的任务还可以放在web worker里面。举一个活生生的例子就是事件回调。
A-》click —-function B(){ fetch(xxx).then(res=>{ }) }
比如A元素绑定了回调事件B,那么A点击时,就会把B事件放到任务队列里面,此时B可以看做是一个宏任务。B函数中的fetch是一个接口请求,是一个微任务。执行顺序如下:
- A元素在鼠标弹起触发时,触发click事件,此时会把B函数放到宏任务队列中
- 浏览器空闲,取宏任务队列B来执行,遇到fetch函数,放到微任务队列中
- B执行完,从微任务队列中逐条取出任务来执行,直到执行完,
- 检查宏任务队列有没有任务,没有就idle,有就取出来执行
代码分割与打包优化
减少代码体积最先想到的办法就是代码分割和打包优化,代码分割的方法就是按需引用,功能模块划分。打包优化我们可以借助webpack,rollup,gulp等这些打包工具来实现。
- 按需引用 (避免打包进去许多无用的代码)
- 打包代码拆分(参考 React基于webpack做code splitting方法)
PS:简单说一些按需引用,随着工程应用的复杂程度提升,我们的代码都是模块化的,那么我们划分模块的思路有两部分:
- 根据是否通用划分成通用模块和非通用模块,通用模块当然可以单独打包,单独加载。然后可以按页面打包,每个页面加载一份业务代码和通用代码
- 对于需要考虑性能体验的页面,可以将页面进一步细分,划分成首屏和非首屏两部分模块,由首屏模块来异步加载非首屏模块。
项目架构优化
这里说的VM架构项目泛指应用了诸如React,pReact,Vue,Angular等前端框架的项目。有时候我们的页面比较复杂,比如淘宝的SKU选择页面,选择商品属性后要伴随价格和库存,图片等许多信息变化,state状态频繁发生变化的这种需求页面,就比较适合用框架来处理。这样代码逻辑更健壮,结合单向数据流处理的便利,更容易开发出高质量代码。
在这里,我们对比了4个流行的框架(库):react, vue, angular1, angular2 ,在最小化情况下的启动性能。对比条件如下:
- 使用每个框架写了一个 Hello World 级别的 App。
- 不引入 redux 这种库,忽略不同框架功能上的不同,目标是做到代码最小。
- angular2 分为 AOT 和 非 AOT 两种 build 方式。
- vue 使用 runtime only 的版本。
- 由于 Angular2 要正常工作,需要预先引入两个polyfill: “es7-reflect” , “zone.js” ,这两个库被单独打包在另外一个文件里,所以结果中除了 Angular2 本身的性能,还包含这两个被依赖的 js 库的性能。

从结果中可以得出结论:
- vue 的启动性能最好,明显优于其他的几个框架。
- Angular2 的 AOT with tree shaking 相对于非 AOT 的版本,性能提升明显,但依然落后于 vue 和 react。
- Chrome 的启动性能要明显慢于 Safari。
- 低端 Android 机器的启动性能非常差,和 iPhone 的差距 10 倍左右,当然价格也接近 10 倍 :)
所以,对于框架选择,优先建议vue、react,然后是考虑Angular,当然如果追求性能体验,做页面轻量化,还是考虑原生es6结合ES6 module来做开发架构。
项目架构优化
ES6module+template
如果是单纯展示性页面,比如前台的首页,宣传页,活动页,类目页,详情页等等。可以用比较简单的轻量化加购,直接用es6 module做模块化拆分,根据首屏、非首屏拆分页面模块,组件生命周期自治,没必要做数据监听,模板采用es6模板即可。结合rollup打包(参考这里: ES6模块打包工具—Rollup速览),能满足性能的要求,开发简单,容易维护。
github代码: https://github.com/chalecao/es6-rollup-template
web component
使用webcomponents,首先考虑下w3c最近新的标准规范中的一些技术:
- Custom Elements
- Shadow DOM
- Custom CSS properties
- JS modules
- 基于import()动态加载模块
- Promise
- class syntax
- Object rest/spread properties
- async/await
截止到目前(2019-1-1)Chrome 60 和 Safari 11.1 及以上版本已经原生支持上面所有的特性, 如果在其他浏览器中使用, 你需要加载一些polyfills组合,使用babel来做代码编译。
特性 | 解决方案 | 适用的浏览器 |
---|---|---|
Custom Elements | Polyfill | IE11, Edge, Firefox<63, Safari<11 |
Shadow DOM | Polyfill | IE11, Edge, Firefox<63, Safari<11 |
Class syntax | Transpile (babel), extra adapter for Custom Elements/Shadow DOM | IE11 |
Promises | Polyfill | IE11 |
Object rest/spread properties | Transpile (babel) | IE11, Edge, Safari<11.1 |
JS modules | Polyfill | IE11, Firefox<60, Safari<11 |
Dynamic JS modules (import()) | Polyfill, a module loader (webpack) | IE11, Edge, Firefox, Safari<11.1 |
async/await | Transpile (babel) | IE11 |
关于如何使用先看下这两篇博文: Shadow DOM简单了解 web components入门与实践
个人比较推荐推荐Polymer方案
轻react或vue
做2c的业务时,推荐经理采用轻量化的方案,这样比较容易做性能优化。做2b,中后台的业务建议采用业界成熟的方案,React技术栈或者Vue技术栈。
我用过preact,不得不说还是有些小坑的。如果单用preact和router或者redux还好,如果用第三方组件,比如preact+ant design mobile时,要注意对应版本关系,否则会出现莫名其妙的错误,此时的解决办法就是锁版本号或者升级版本。
React的资源很多,还是参考下官方,还有新特性介绍:官方资源
Vue:官方文档
微前端
大家可能之前都听说过后端的微服务架构,这里说的微前端其实有类似的思想,主要是用来解决前端的异构问题。比如一些跨项目的大工程,整合几个web应用,需要考虑不同的前端团队项目,极有可能是不同的前端技术框架。
关于微前端架构的思想可以看看这里:[微前端架构的一些想法]41
微前端最早是2016年 ThoughtWorks Technology Radar 提出,主要是解决遗留的大工程项目后期维护迁移的问题,如何与现有其他系统打通等等。微前端背后的思想是一个公司开发的产品应该是一套整体的应用,每个团队只是负责其中的一部分端到端的开发,整个应用是这些feature的集合。
微前端的核心思想:
- 技术方案隔离
不同的技术团队可以采用各自的技术栈,可以自己维护升级,不会影响其他团队技术方案。 Custom Elements 是一个可以实现隐藏元素内部细节的技术隔离方案。 - 代码隔离
不同模块之间的代码互不影响,可以不在同一个上下文环境,不在同一个运行时。不依赖于共享的状态和全局变量。 - 建立团队共同的编码规范
建立共同的代码编写缩写组合规范,比如CSS命名空间, Events, Local Storage and Cookies 来避免冲突 - 尽量使用浏览器支持的特性进行通信,不要自己处理通信方案
用 Browser Events for communication 而不是自己创建一个发布/订阅系统. 如果非要提供一个跨团队、跨项目的API, 尽量越简单越好 - 采用适应力强可伸缩的方案
每个功能特性应该保证稳定性可用性,即使某个模块的脚本出问题了。可以采用 Universal Rendering或者渐进增强的技术方案提升用户体验
基于web component实现微前端的架构方案实战看这里:[微前端基于webcomponents的实现]46
参考
Planning for Performance Solving the Web Performance Crisis by Nolan Lawson JS Parse and Execution Time Measuring Javascript Parse and Load Unpacking the Black Box: Benchmarking JS Parsing and Execution on Mobile Devices (slides) When everything’s important, nothing is! The truth about traditional JavaScript benchmarks Do Browsers Parse JavaScript On Every Page Load javascript-start-up-performance thinking-prpl micro-frontends Talk: Micro Frontends - Web Rebels, Oslo 2018 (Slides) Slides: Micro Frontends - JSUnconf.eu 2017 Talk: Break Up With Your Frontend Monolith - JS Kongress 2017 Elisabeth Engel talks about implementing Micro Frontends at gutefrage.net Post: Micro frontends - a microservice approach to front-end web development Tom Söderlund explains the core concept and provides links on this topic Post: Microservices to Micro-Frontends Sandeep Jain summarizes the key principals behind microservices and micro frontends Link Collection: Micro Frontends by Elisabeth Engel extensive list of posts, talks, tools and other resources on this topic Awesome Micro Frontends a curated list of links by Christian Ulbrich 🕶 Custom Elements Everywhere Making sure frameworks and custom elements can be BFFs