We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
上次的文章给大家分享了怎么在webpack打包阶段去将自己的项目优化到极致。文章链接:将webpack打包优化到极致_20180619
H5页面的性能瓶颈,网络因素几乎可以占到80%。不管是减小产出文件体积,还是使用HTTP2.0或者是PWA等都是减少网络对H5页面加载的影响。
HTTP2.0
PWA
我们部门产品主要是在拉美国家使用。拉美国家的用户有个比较明显的特点:
下图是从公司的h5性能监控平台上查询到的信息。
从上图可以看出大部分用户所在的网络环境还是很不错的。但是前线的同事还是经常会反馈我们的H5页面在用户手机上打开缓慢。我们讨论后有两个猜测:
有了猜测和因为,我们就针对上面提到的问题,通过技术手段进行了探究。探究依赖的理论知识如下:
上图体现了当我们在浏览器地址栏中输入地址按下 enter 键后,到页面加载出来页面的整个生命周期。浏览器(或者是webview)是提供了 Performance API让我们获取各个阶段开始执行的时间的。
Performance
查看文档根据Performance API,编写了上报数据的脚本。 脚本链接请点击这里
我把代码主要部分和需要注意的地方拿出来,大家在用的时候可以注意一下:
// 注意在实践中发现一些旧款的安卓机型是没有 getEntries 方法的,所以脚本要做兼容 const entryList = performance && performance.getEntries && performance.getEntries(); // 一般情况下我们都会将css放在head标签中,js放在body标签底部。如果静态文件使用了单独的CDN域名这是只需要获取第一个加载的CSS的对象就行 for (let i = 0; i < entryList.length; i++) { const obj = entryList[i]; if (obj.initiatorType === 'link') { linkPerformance = obj; break; } } // 下面是获取的各个阶段的开始时间 const cssDomainlookStart = linkPerformance ? linkPerformance.domainLookupStart : 0; const cssDomainlookEnd = linkPerformance ? linkPerformance.domainLookupEnd : 0; const connectStart = linkPerformance ? linkPerformance.connectStart : 0; const connectEnd = linkPerformance ? linkPerformance.connectEnd : 0; const requestStart = linkPerformance ? linkPerformance.requestStart : 0; const responseStart = linkPerformance ? linkPerformance.responseStart : 0; const responseEnd = linkPerformance ? linkPerformance.responseEnd : 0; // Omega是公司统一的上报数据的脚本。 下面是计算每个阶段的开始和结束的时间差 Omega && Omega.trackEvent && Omega.trackEvent('static_domain_timing', '', { country: country, lookupTiming: cssDomainlookEnd - cssDomainlookStart, connectTiming: connectEnd - connectStart, requestTiming: responseStart - requestStart, responseTiming: responseEnd - responseStart, host: 'static.didiglobal.com', cityid });
因为数据是通过脚本上报先写入数据库,然后通过SQL脚本查出来的。所以均以表格的形式来展现。
当我们有了以上统计数据的时候,我们得出的结论是:
总体上在拉美主要城市我们的page域名和static域名运行的网络环境还是很不错的。存在慢的情况,但是与总人数相比是可以忽略不计的。
上面的结论并不是说目前我们业务的网络环境已经没有优化空间了,只是跟据优化成本和紧切度来说,目前的状态还是能够满足需求的。不过后续我们有推动运维部门将我们的网络协议升级为 HTTP2.0。
排除了网络因素对我们页面加载速度的影响,那么到底是什么原因影响页面的加载速度呢?因为我们的项目100%都是使用vue的单页应用,单页应用的一个最大的缺点这时就暴露了出来,**“首屏”**慢。
vue
由以上我们推理出来3种情况,我们提出了一个假设:
假如通过某种方式提前缓存渲染h5需要的html和静态文件(JS,CSS)是不是可以加快首屏的渲染速度,具体又能提高多少,是否有大规模推广的必要?。
为了验证 4.1 中的猜想,我们需要数据的支撑。紧接着我们找客户端同学跟我们一起做了几次实验。
在客户端发版的时候,提前将一些“单页”H5渲染所需要的资源跟随客户端一起打包发版,然后对这些页面的加载速度进行监控。
下面是我们对预缓存到客户端页面的前后渲染时间统计对比得出的一些数据:
变化计算公式:(优化前平均值-优化后平均值)/ 优化前平均值
由以上数据作为理论支撑,得出的结论是提前缓存静态资源到客户端内,可以明显加快页面的渲染速度。
在4.2中为了验证预缓存静态资源到客户端内对H5页面渲染的影响,采用的是手动的方式。需要每次提前跟客户端沟通发版时间,提前将我们需要缓存的资源手动打包好发给客户端的同学。
这样的方式有很多缺点:
有这么多的缺点,如果采用这种方式想要在我们的业务线推广显然是不可取的。下面我们就设计了一套自动化缓存静态资源到客户端内的方案。
要实现自动化缓存方案,客户端和前端肯定是要密切配合的。因为我主要是负责前端这块,客户端的内容这篇内容我就简单的介绍下。如果有同学想深入的探讨可以给我留言。
基本的流程可以参考下图:
上面只是一个简单的流程。在实际的开发中还需要对 url 和 本地缓存文件建立映射关系,缓存的删除,以及缓存模块的下线等。
前端项目上线流程基本都是下面这样子的。
当我们在设计前端离线缓存方案的时候,一个需要考虑的重要因素就是,这个方案不能对我们现有业务和build,上线流程有太大的影响,换句话说我们的旧项目只要简单的修改就可以接入这一套离线缓存方案。
最后我们的的方案如下图所示:
上面的这套方案依赖的理论就是 CDN 就是来分担服务器压力,分流,提供文件下载,加速等而设计的。为了减少客户端在启动时对需要缓存的静态资源请求数,在build阶段会将需要缓存的资源统一打包成zip文件,供客户端下载。
上一节的内容,可以知道我们是想将生成zip缓存包的时间放在build阶段。因为我们的所有项目都是采用webpack进行构建的。所以就开发了一个webpack插件来做这件事情。
我们项目中使用的插件叫 webpack-static-chache-zip。这个插件我做 webpack2.0+ 和 webpack4.0 的兼容。
webpack2.0+
webpack4.0
插件的功能点如下:
详细的可以参考一下的配置:
const AkWebpackPlugin = require('webpack-static-chache-zip'); // 初始化插件 new AkWebpackPlugin({ // 最终生成的离线包名称,默认值是 `offline` 'offlineDir': 'offline', // 生成环境的代码源,默认值 `output` webpack编译产出生产环境代码的目录 'src': 'output', // 是否保留生成的离线包文件夹(zip包的源文件) 'keepOffline': true, // datatype: [必填] 1(Android乘客端),2(Android司机端),101(iOS乘客端),102(iOS司机端) 'datatype': '', // terminal_id 业务名称,比如乘客端钱包,不能重复 具体查看wiki http://wiki.intra.xiaojukeji.com/pages/viewpage.action?pageId=118882082 'terminal_id': '', // 如果存在一个data_type 对应多个terminal_id 的情况。 可以按照下面的方法以数组对象的方式列出来 'terminal_list': [ { data_type: 1 terminal_id: 2 }, { data_type: 1 terminal_id: 2 } ... ], // 想要包含的文件路径,模糊匹配,优先级高于 excludeFile 'includeFile': [ 'balance_topup', 'static', 'pay_history' ], // 需要排除的文件路径 模糊匹配优先级低于 includeFile 'excludeFile': [ 'repay_qa', 'test', 'pay_account', 'fill_phonenum', 'balance_qa', 'select_operator', 'payment_status' ], // 缓存的文件类型,默认是 html js css 如果需要缓存其它类型文件 ['png', 'jpg'] 'cacheFileTypes': [], // 产品线ID 为保持唯一性 接触模块请到 http://wiki.intra.xiaojukeji.com/pages/viewpage.action?pageId=272106764 查看自己使用的module已经被使用 // 接入是也请登记自己的module 'module': 'passenger-wallet', // 页面域名 ,可以配置多个域名,主要适用于一个文件上线后可能被多个域名使用的场景 // 比如: https://aaaa.com/a.html 和 https://bbbb.com/a.html 其实访问是同一个文件只是由于业务场景的不同使用不同的域名 'pageHost': 'https://page.didiglobal.com', // urlpath 'urlPath': '/global/passenger-wallet/', // 这个字段和下面的 patchCdnPath 较为特殊。比如我们打包产出的路径 /xxxx/xx/output/aaa/bb/index.html 在上线的时候实际上是将output目录copy到了 // 服务器上 原理上我们的页面的url 应该是 https://page.didiglobal.com/aaa/bb/index.html 但是某些项目可能为了缩短路径查找 通过ngxin 配置 我们实际访问 // 的 https://page.didiglobal.com/index.html 所在在这里可以配置 patchUrlPath: 'aaa/bb' 'patchUrlPath': '', // cdn域名 静态文件域名(js/css/html) 如果不配置或者设置为空数组会默认使用pageHost 'cdnHost': 'https://static.didiglobal.com', // cdnpath 如果不设置会默认使用 urlPath 'cdnPath': '', // 参考上面的patchUrlPath使用方法 'patchCdnPath': '', // zip文件的域名如果不设置会默认使用 cdnHost 'zipHost': '', // zipPath 如果不设置会默认使用cdnPath 'zipPath': '', // 一个H5页面会跑在不同端内(比如我们巴西和global司机是两个单独的客户端),这两个端内的h5页面又有不同的 // page域名和static域名 这个时候可以通过otherHost配置来设置环境页面和静态文件的域名, // 可以不设置或者为空 'otherHost': { // 页面的域名 'page': 'page.99taxis.mobi', // 可以设置单独的 cdn域名如果不设置则与page域名相同 'cdn': 'static.99taxis.mobi' }, // 压缩参数,详参 https://archiverjs.com 'zipConfig': {zlib: {level: 9} }, // 下列回调方法,可以直接使用this.fs (fs-extra), this.success, this.info, this.warn, this.alert // 在 拷贝文件到 offline 离线文件夹之前 beforeCopy: function () {}, // 在 拷贝文件到 offline 离线文件夹之后 afterCopy: function () { }, // 在压缩 offline 离线文件夹之前 beforeZip: function (offlineFiles) { // offlineFiles 在离线包文件夹内的文件路径信息 }, // 在压缩 offline 离线文件夹之后 afterZip: function (zipFilePath) { // zipFilePath 最终生成的离线zip包路径 } })
以下是插件的工作流程
include
exclude
这里的版本diff是指,第一次上线全量的离线包之后,为了节省用户流量和用户手机的磁盘空间,第二次到第N次上线,我们应该上线的都是diff过后的zip包,并且需要将一些旧的文件信息告诉客户端端,让客户端进行删除操作(节省用户磁盘空间)。
文件信息和diff信息都应该使用存储服务(数据库)存储起来的。但是为了加快编译速度在编译和diff的过程中是不和服务器进行接口请求发生数据交换的。所有信息都以json文件的形式存储在本地。在编译上线完之后将信息再统一发送给服务器存储。
json文件
diff的基本流程如下图:
我们的需求是只在线上缓存5个版本的离线包。也就是说每次上线的版本都会跟之前的5个版本发生组合关系。根据高中时学习的组合公式,我们应该最多需要缓存10个diff版本和1个全量版本。也就是11个版本。
因为客户端在下载完离线缓存资源之后需要校验离线文件的完整性的。所以在生成zip包的时候插件会计算每个需要缓存文件的MD5,客户端在下载完之后也计算MD5 ,跟我的MD5进行比较。
因为我在build阶段会计算一次文件的MD5,在diff的时候我会将这个MD5值作为一个文件的唯一标识。
版本的diff原理可以简化为一下两个数组的比较。如下图:
以下是核心的代码示例,可能我的实现不是最好的,如果大佬有更好的实现方法可以留言给我(手动感激)
// 有两个数组 oldArr和newArr ,oldArr变成newArr后那些元素是新增的,哪些元素是应该被删除的 // 我们可以给元素添加字段type 来做标记 0标记需要删除 1 标识已有的 2标记元素是新增的 const oldArr = [ {tag: 1}, {tag: 2}, {tag: 3}, {tag: 4} ]; const newArr = [ { tag: 3 }, { tag: 4 }, { tag: 5 }, { tag: 6 } ] const newArrTag = []; // 需要删除的 const delArrList = []; for (let i = 0; i < newArr.length; i++) { const newItem = newArr[i]; // 默认将newArr每一项都设置为新增 newItem.type = 2; newArrTag.push(newItem.tag); for (let m = 0; m < oldArr.length; m++) { const oldItem = oldArr[m]; if(newItem.tag === oldItem.tag ) { newItem.type = 1 } } } for (let n = 0; n < oldArr.length; n++) { const oldItem = oldArr[n]; if(!newArrTag.includes(oldItem.tag)) { oldItem.type = 0 delArrList.push(oldItem); } } const resultArr = newArr.concat(delArrList); console.log(resultArr) // 以下是输出结果,将oldArr变化成newArr 之后每个元素是新增,应该删除,还是使用旧的都做了标记 [ { tag: 3, type: 1 }, { tag: 4, type: 1 }, { tag: 5, type: 2 }, { tag: 6, type: 2 }, { tag: 1, type: 0 }, { tag: 2, type: 0 } ]
以上的代码就是版本diff的流程。当然与webpack和业务结合起来比复杂一些。有兴趣的同学可以查看前面的插件链接,读一下源码。
下面是我们的乘客端钱包上线前3天和上线后3天的数据统计:
下图红框中是我们接入离线缓存的页面:
由上图的数据对比可以发现,使用了离线缓存的页面,Dom ready的时间都明显提升了几乎 90% 。
Dom ready
下图是公司对Web页面性能指标统计平台,统计的数据:
根据以上的统计结果:对乘客端的所有的H5页面均加载时间缩短了 300ms。 domready的时间也缩短了接近 300ms。 **其实这么对比是不准确的,因为这个数据是包含了非接入离线缓存页面的数据。这里只能看到所有页面的均值。**这小结的开头的数据统计可以反映出离线缓存对页面加载时间的影响。
300ms
domready
这篇文章介绍了我们团队为了优化H5页面在客户端加载速度,在静态文件离线缓存方面做得一些前期调研和实际开发工作,以及到最后工程化的实践。总体对我们页面加载速度的提升还是很明显的。只需要以webpack插件的方式接入,不需要对现有前端项目做太多改造,对前端工程师工作效率提高也很大。
篇幅有限,文章中有些内容可能介绍的还是比较笼统模糊,有兴趣的同学或者是想在自己的团队实践的同学,有问题可以留言讨论。
The text was updated successfully, but these errors were encountered:
5.1 客户端侧 客户端拦截后,先查看是否有缓存,这个缓存是指的数据库里存的还是浏览器的缓存呢,我这边遇到一个浏览器缓存失效的问题,只要是没有预下载的文件,再次进入没有走浏览器缓存,都会重新请求,平均下来速度反而慢了,我们用的 work-box,可以交流一下吗
Sorry, something went wrong.
我们这个是自己设计的文件维度的缓存,跟浏览器的缓存还不是一个概念。你们缓存失效的问题,看看是不是你们客户端的bug,对于没有预下载的文件都是直接走网络请求,没有使用浏览器的缓存。
No branches or pull requests
前端性能优化之----静态文件客户端离线缓存
1. 前言
上次的文章给大家分享了怎么在webpack打包阶段去将自己的项目优化到极致。文章链接:将webpack打包优化到极致_20180619
2. 探究业务的瓶颈
H5页面的性能瓶颈,网络因素几乎可以占到80%。不管是减小产出文件体积,还是使用
HTTP2.0
或者是PWA
等都是减少网络对H5页面加载的影响。我们部门产品主要是在拉美国家使用。拉美国家的用户有个比较明显的特点:
下图是从公司的h5性能监控平台上查询到的信息。
从上图可以看出大部分用户所在的网络环境还是很不错的。但是前线的同事还是经常会反馈我们的H5页面在用户手机上打开缓慢。我们讨论后有两个猜测:
3. 网络因素的探究
有了猜测和因为,我们就针对上面提到的问题,通过技术手段进行了探究。探究依赖的理论知识如下:
上图体现了当我们在浏览器地址栏中输入地址按下 enter 键后,到页面加载出来页面的整个生命周期。浏览器(或者是webview)是提供了
Performance
API让我们获取各个阶段开始执行的时间的。查看文档根据
Performance
API,编写了上报数据的脚本。 脚本链接请点击这里我把代码主要部分和需要注意的地方拿出来,大家在用的时候可以注意一下:
3.1 对html域名的数据调研结果
因为数据是通过脚本上报先写入数据库,然后通过SQL脚本查出来的。所以均以表格的形式来展现。
3.2 对CDN域名的数据调研结果
总结
当我们有了以上统计数据的时候,我们得出的结论是:
总体上在拉美主要城市我们的page域名和static域名运行的网络环境还是很不错的。存在慢的情况,但是与总人数相比是可以忽略不计的。
4. 问题猜想以及优化方案
上面的结论并不是说目前我们业务的网络环境已经没有优化空间了,只是跟据优化成本和紧切度来说,目前的状态还是能够满足需求的。不过后续我们有推动运维部门将我们的网络协议升级为 HTTP2.0。
4.1 问题的猜想
排除了网络因素对我们页面加载速度的影响,那么到底是什么原因影响页面的加载速度呢?因为我们的项目100%都是使用
vue
的单页应用,单页应用的一个最大的缺点这时就暴露了出来,**“首屏”**慢。由以上我们推理出来3种情况,我们提出了一个假设:
假如通过某种方式提前缓存渲染h5需要的html和静态文件(JS,CSS)是不是可以加快首屏的渲染速度,具体又能提高多少,是否有大规模推广的必要?。
4.2 数据结果
为了验证 4.1 中的猜想,我们需要数据的支撑。紧接着我们找客户端同学跟我们一起做了几次实验。
在客户端发版的时候,提前将一些“单页”H5渲染所需要的资源跟随客户端一起打包发版,然后对这些页面的加载速度进行监控。
下面是我们对预缓存到客户端页面的前后渲染时间统计对比得出的一些数据:
变化计算公式:(优化前平均值-优化后平均值)/ 优化前平均值
由以上数据作为理论支撑,得出的结论是提前缓存静态资源到客户端内,可以明显加快页面的渲染速度。
5. 自动化的离线缓存方案
在4.2中为了验证预缓存静态资源到客户端内对H5页面渲染的影响,采用的是手动的方式。需要每次提前跟客户端沟通发版时间,提前将我们需要缓存的资源手动打包好发给客户端的同学。
这样的方式有很多缺点:
有这么多的缺点,如果采用这种方式想要在我们的业务线推广显然是不可取的。下面我们就设计了一套自动化缓存静态资源到客户端内的方案。
5.1 客户端侧
要实现自动化缓存方案,客户端和前端肯定是要密切配合的。因为我主要是负责前端这块,客户端的内容这篇内容我就简单的介绍下。如果有同学想深入的探讨可以给我留言。
基本的流程可以参考下图:
上面只是一个简单的流程。在实际的开发中还需要对 url 和 本地缓存文件建立映射关系,缓存的删除,以及缓存模块的下线等。
5.2 前端侧
前端项目上线流程基本都是下面这样子的。
当我们在设计前端离线缓存方案的时候,一个需要考虑的重要因素就是,这个方案不能对我们现有业务和build,上线流程有太大的影响,换句话说我们的旧项目只要简单的修改就可以接入这一套离线缓存方案。
最后我们的的方案如下图所示:
上面的这套方案依赖的理论就是 CDN 就是来分担服务器压力,分流,提供文件下载,加速等而设计的。为了减少客户端在启动时对需要缓存的静态资源请求数,在build阶段会将需要缓存的资源统一打包成zip文件,供客户端下载。
6. 开发webpack插件
上一节的内容,可以知道我们是想将生成zip缓存包的时间放在build阶段。因为我们的所有项目都是采用webpack进行构建的。所以就开发了一个webpack插件来做这件事情。
我们项目中使用的插件叫 webpack-static-chache-zip。这个插件我做
webpack2.0+
和webpack4.0
的兼容。插件的功能点如下:
详细的可以参考一下的配置:
6.1 插件工作流程
以下是插件的工作流程
include
和exclude
来判断文件 文件是否需要拷贝6.2 版本diff
这里的版本diff是指,第一次上线全量的离线包之后,为了节省用户流量和用户手机的磁盘空间,第二次到第N次上线,我们应该上线的都是diff过后的zip包,并且需要将一些旧的文件信息告诉客户端端,让客户端进行删除操作(节省用户磁盘空间)。
文件信息和diff信息都应该使用存储服务(数据库)存储起来的。但是为了加快编译速度在编译和diff的过程中是不和服务器进行接口请求发生数据交换的。所有信息都以
json文件
的形式存储在本地。在编译上线完之后将信息再统一发送给服务器存储。diff的基本流程如下图:
我们的需求是只在线上缓存5个版本的离线包。也就是说每次上线的版本都会跟之前的5个版本发生组合关系。根据高中时学习的组合公式,我们应该最多需要缓存10个diff版本和1个全量版本。也就是11个版本。
6.3 diff原理
因为客户端在下载完离线缓存资源之后需要校验离线文件的完整性的。所以在生成zip包的时候插件会计算每个需要缓存文件的MD5,客户端在下载完之后也计算MD5 ,跟我的MD5进行比较。
因为我在build阶段会计算一次文件的MD5,在diff的时候我会将这个MD5值作为一个文件的唯一标识。
版本的diff原理可以简化为一下两个数组的比较。如下图:
以下是核心的代码示例,可能我的实现不是最好的,如果大佬有更好的实现方法可以留言给我(手动感激)
以上的代码就是版本diff的流程。当然与webpack和业务结合起来比复杂一些。有兴趣的同学可以查看前面的插件链接,读一下源码。
7.上线后数据统计
下面是我们的乘客端钱包上线前3天和上线后3天的数据统计:
下图红框中是我们接入离线缓存的页面:
由上图的数据对比可以发现,使用了离线缓存的页面,
Dom ready
的时间都明显提升了几乎 90% 。下图是公司对Web页面性能指标统计平台,统计的数据:
根据以上的统计结果:对乘客端的所有的H5页面均加载时间缩短了
300ms
。domready
的时间也缩短了接近300ms
。 **其实这么对比是不准确的,因为这个数据是包含了非接入离线缓存页面的数据。这里只能看到所有页面的均值。**这小结的开头的数据统计可以反映出离线缓存对页面加载时间的影响。总结:
这篇文章介绍了我们团队为了优化H5页面在客户端加载速度,在静态文件离线缓存方面做得一些前期调研和实际开发工作,以及到最后工程化的实践。总体对我们页面加载速度的提升还是很明显的。只需要以webpack插件的方式接入,不需要对现有前端项目做太多改造,对前端工程师工作效率提高也很大。
篇幅有限,文章中有些内容可能介绍的还是比较笼统模糊,有兴趣的同学或者是想在自己的团队实践的同学,有问题可以留言讨论。
The text was updated successfully, but these errors were encountered: