本文首发于 vivo互联网技术 微信公众号
连接: https://mp.weixin.qq.com/s/6gtVR0nVNcZvREjwftZgzA
做者:悟空中台研发团队javascript
【悟空活动中台】系列往期精彩文章:css
经过以前悟空活动中台系列文章,你们对微组件、动态布局等技术方案有了必定的了解。本篇咱们带你们了解下悟空H5专题性能优化之路。html
在移动互联网时代,H5页面加载体验相当重要。消费者行为和观念也会受到页面加载时间的产生显着影响,最明显的就是咱们如今很难去等待一个页面加载超过三秒的页面,尤为是年轻人。专一性能测试的SOASTA公司曾发表过结论:移动端加载每耗时1秒, 影响转化率最高可达 20%。前端
在营销中台业务快速发展过程当中,悟空始终把网站响应速度和用户体验放在第一位,经过技术创新,不断寻找最优加载方案,取得了很好的效果。下面咱们就一块儿来探索下。vue
每谈到性能优化,前端er就能联想到一道经典面试题:从输入URL到页面加载,浏览器都执行了什么?java
体验优化的历程和这道题同样,须要系统化梳理、体系化实践。咱们从网络、资源、渲染、执行层出发,不断探索加载优化方案。webpack
浏览器对网站第一次的域名 DNS 解析查找流程依次为:浏览器缓存 >> 系统缓存 >> 路由器缓存 >> ISP DNS 缓存 >> 递归搜索。css3
移动端环境下,DNS 请求带宽很是小,但延迟很高。针对该问题,咱们采起预读取DNS方案,该方案能显著下降延迟,平均加载时长可减小1秒左右。git
为帮助浏览器对某些域名进行预解析,咱们对上线活动 html 文档中新增 dns-prefetch标签。加入该标签后,浏览器解析步骤以下:github
第一步:用 meta 信息来告知浏览器,当前页面要作 DNS 预解析:
<meta http-equiv="x-dns-prefetch-control" content="on" />
第二步:在页面 header 中使用 link 标签来强制对 DNS 预解析:
<link rel="dns-prefetch" href="//topicstatic.vivo.com.cn" />
悟空在上线H5资源须要根据不一样区域,生成不一样的dns-prefetch地址,编译活动脚手架link标签新增逻辑以下:
<% if (国内活动) {%> <link rel="dns-prefetch" href="//topic.vivo.com.cn"> <link rel="dns-prefetch" href="//cmsapi.vivo.com.cn"> <link rel="dns-prefetch" href="//topicstatic.vivo.com.cn"> <% } else if(印度活动) {%> <link rel="dns-prefetch" href="//in-goku.vivoglobal.com"> <link rel="dns-prefetch" href="//topicstatic.vivo.com.cn"> <link rel="dns-prefetch" href="//in-gokustatic.vivoglobal.com"> <% } else { %> <link rel="dns-prefetch" href="//asia-goku.vivoglobal.com"> <link rel="dns-prefetch" href="//asia-gokustatic.vivoglobal.com"> <link rel="dns-prefetch" href="//asia-wukongapi.vivoglobal.com"> <% } %>
CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,经过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,下降网络拥塞,提升用户访问响应速度和命中率。
下图展现终端用户访问页面时,CDN获取过程:
缓存对于CDN服务相当重要,合适的缓存策略可以下降源站的请求压力,从而提高页面加载速度,所以咱们须要优化静态资源存储方式和缓存策略。
CDN资源缓存配置以下:
悟空将H5专题的静态资源上传至CDN,带来以下提高:
不缓存HTML入口文件的目的是防止客户端缓存策略,致使主入口资源不更新,致使线上升级失败。
HTTP/2 的定义为:
(超文本传输协议第 2 版,最初命名为HTTP 2.0),简称为h2(基于 TLS/1.2 或以上版本的加密链接)或h2c(非加密链接)[1],是HTTP协议的的第二个主要版本,使用于万维网。
将 HTTP 消息分解为独立的帧,交错发送,而后在另外一端从新组装是 HTTP 2 最重要的一项加强。事实上,这个机制会在整个网络技术栈中引起一系列连锁反应,从而带来巨大的性能提高:
_ |
1.0 |
1.1 |
2.0 |
长链接 |
须要使用keep-alive 参数来告知服务端创建一个长链接 |
默认支持 |
默认支持 |
HOST 域 |
不支持 |
支持 |
支持 |
多路复用 |
不支持 |
- |
支持 |
数据压缩 |
不支持 |
不支持 |
使用HAPCK算法对 header 数据进行压缩,使数据体积变小,传输更快 |
服务器推送 |
不支持 |
不支持 |
支持 |
HTTP2.0开启方式以下:
server { listen 443 **ssl** **http2**; server_name yourdomain; …… ssl on**; …… }
开启 HTTP 2监听:
listen 443 ssl http2;
多路复用代替原有的序列以及阻塞机制,使得多个资源能够在一个链接中并行下载,不受浏览器同一域名资源请求限制,提高整站的资源加载速度。
字体文件大小广泛在2M左右,H5活动页面字体量有限,但仅仅为少许特殊文字全量引入字体文件,页面性能损耗很是大。与此同时,因为营销活动的复杂性与多样性,单纯的图片字体很难知足多变的运营需求。
寻找知足字体多样性的同时,保证字体大小,是平台需攻克的技术难点,最终,咱们探索出一套适用平台的动态字体压缩方案。
字体压缩,也能够被称为字体子集化,能够理解为经过特定方式将中英文字从大字体文件中剥离,组合成小字体文件供页面使用。
概念看上去有点抽象,咱们先直观感觉下压缩先后效果:
接下来会重点讲述悟空基于业务场景的字体压缩方案,压缩字体的核心诉求是:可压缩字体文件,可动态更换文本内容进行压缩。
基于悟空微组件动态打包上线方式,咱们选择使用 fontmin 来完成动态压缩字体。
动态压缩字体分为如下几个步骤:
第一步,读取特定配置文件中的 id,预先请求到对应页面接口数据,进行数据归集处理。部分代码示例:
const request = require('request') request(url, (error, response, data) => { if (error) { console.error(err); return } const res = JSON.parse(data) if (res.code === 0) { //获取专题配置数据 const config = JSON.parse(URLDecode(res.data.config)) const pages = config.pages let str = '' const familyList = new Set() pages.forEach(page => { const items = page.items items.forEach(item => { //根据配置,拼接需加载字体的字符串和字体类型 if (item.pluginInfo.enName === 'site-text') { str += item.pluginConfig.pureText familyList.add(item.pluginConfig.typeFace) } }) }); //处理字体 handleFont(str, familyList) } });
第二步,遍历字体类型列表 familyList,利用 fontmin 进行字体文件压缩。这一步要求咱们预先将字体的本地文件放入编译脚手架中。在压缩的同时,须要经过webpack插件来生成对应的 css 文件:
字体动态压缩处理逻辑:
const compressFont = (fontText, fontName) => { const srcPath = `dist/${siteId}/font/${fontName}.ttf`; const destPath = `dist/${siteId}/compressFont`; const fontmin = new Fontmin() .src(srcPath) // 输入配置 .use(Fontmin.glyph({ // 字形提取 text: fontText // 动态注入文字 })) .use(Fontmin.ttf2eot()) // eot转换 .use(Fontmin.ttf2woff()) // woff转换 .use(Fontmin.ttf2svg()) // svg转换 .use(Fontmin.css({ fontPath: `/compressFont/`, fontFamily: fontName, })) .dest(destPath); // 输出文件 fontmin.run(function (err, files, stream) { if (err) { console.error(err); return } // 读取生成后的对应的 css 文件内容并合成 const fontCss = fs.readFileSync(path.join(__dirname, `../dist/${siteId}/compressFont/${fontName}.css`)).toString() fontStyleStr += fontCss loadHtml(fontStyleStr) }) } const handleFont = (fontText, familyList) => { familyList.forEach(name => { compressFont(fontText, name) }) }
图片懒加载是一种很好的优化网页或应用的方式,它可以在用户滚动页面时自动获取更多的数据,新获取的图片不会影响到页面呈现,同时视口外的图片有可能永远不须要被加载,可以极大的节约用户流量以及服务器资源。'
懒加载的通常形式表现为:
根据悟空现有的技术栈,咱们选择vue-lazyload 去支撑位组件的图片来加载:
悟空提供给组件开发者资源懒加载指令,用户无需感知具体的加载逻辑,经过悟空的内置能力便可实现专题图片懒加。具体用法以下:
<template> <div> <img v-lazy="imgUrl" /> <div v-lazy:background-image="imgUrl"></div> <!-- with customer error and loading --> <img v-lazy="imgObj" /> <div v-lazy:background-image="imgObj"></div> <!-- Customer scrollable element --> <img v-lazy.container="imgUrl" /> <div v-lazy:background-image.container="img"></div> <!-- srcset --> <img v-lazy="'img.400px.jpg'" data-srcset="img.400px.jpg 400w, img.800px.jpg 800w, img.1200px.jpg 1200w" /> <img v-lazy="imgUrl" :data-srcset="imgUrl' + '?size=400 400w, ' + imgUrl + ' ?size=800 800w, ' + imgUrl +'/1200.jpg 1200w'" /> </div> </template> <script> export default { data() { return { imgObj: { src: 'http://xx.com/logo.png', error: 'http://xx.com/error.png', loading: 'http://xx.com/loading-spin.svg', }, imgUrl: 'http://xx.com/logo.png', // String } }, } </script>
在移动端环境下,图片加载一直是须要重点优化的关键项,因此才延伸出懒加载这种交互方案来提升用户体验。
当该方案优化到了落地后,咱们下一步考虑如何在保证图片质量的前提下,尽可能压缩图片体积,提高图片加载效率。
WebP 是 Google 推出的一种同时提供了有损压缩与无损压缩(可逆压缩)的图片文件格式。相比于其余相同大小不一样格式的压缩图像,WebP 格式的图片拥有更小的体积以及更高的质量,因此它的优点十分明显。
WebP 是 Google 推出的一种同时提供了有损压缩与无损压缩(可逆压缩)的图片文件格式。相比于其余相同大小不一样格式的压缩图像,WebP 格式的图片拥有更小的体积以及更高的质量,因此它的优点十分明显。
在使用 WebP 进行有损压缩后,咱们大概能够将本来的图片大小压缩至原来的十分之一左右,而图片质量却没有大的损失。这确实是一个惊人的效率。
咱们能够看下一组数据来看下 webp 有损压缩效果:
Webp 有损压缩(75%质量比)
await execFileSync(cwebp, ['-q', '75', filePath, '-o', webpPath]);
原大小 |
压缩时间(ms) |
压缩后大小 |
999kb |
237 |
38kb |
999kb |
221 |
38kb |
999kb |
228 |
38kb |
999kb |
228 |
38kb |
999kb |
261 |
38kb |
在转换结束后,悟空会将原图片和转换后的 webp 图片都上传到 cdn 上,作一个备份的能力,实际业务场景能够根据需求去选择是否使用 Webp 图片。
下图展现 Webp 压缩先后效果,右侧展现压缩后图片,图片大小从215k减少至17k。
悟空在使用 Webp 压缩时,也遇到种种问题,以下:
后续文章《悟空活动中台 - 基于Webp的图片高效加载方案》会详细叙述悟空如何从平台角度提供 Webp压缩方案。
悟空H5专题采用的是先后端分离方案,服务器域名和专题域名不一致,会受到浏览器同源策略影响。
咱们发现数据主接口会发起两次,其中第一个请求为预检请求。
通常来讲使用 application/json 的 post 请求是必然会带入 OPTION 请求,何为 OPTION 预检:
用于获取目的资源所支持的通讯选项。客户端能够对特定的 URL 使用 OPTIONS 方法,也能够对整站(经过将 URL 设置为“*”)使用该方法。
在 CORS 中,可使用 OPTIONS 方法发起一个预检请求,以检测实际请求是否能够被服务器所接受。预检请求报文中的 Access-Control-Request-Method 首部字段告知服务器实际请求所使用的 HTTP 方法;Access-Control-Request-Headers 首部字段告知服务器实际请求所携带的自定义首部字段。服务器基于从预检请求得到的信息来判断,是否接受接下来的实际请求。
有趣的是专题详情为 GET 接口,为什么 GET 请求也会发起 option 预检?
这个缘由得从简单请求和复杂请求提及,跨域请求分为简单和复杂两种:
简单请求:
请求方式为以下之一:
HEAD
GET
POST
HTTP 请求头只能包含以下信息:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type,但仅能是下列之一
application/x-www-form-urlencoded
multipart/form-data
text/plain
任何一个不知足上述要求的请求,即被认为是复杂请求。一个复杂请求不只有包含通讯内容的请求,同时也包含预检信息。
专题配置接口请求头中带有自定义 header,浏览器会认定为非简单请求,须要向服务器发出检查,判断该域名是否容许跨域。
通过分析发现,自定义 header 其实在此业务场景中非必传自带,发出预检请求至少会有 100ms 的耗时,无形中延长页面绘制时间。
最终解决方案:去除自定义header,修改成简单请求,避免该请求发出预检。
在网络层以及资源压缩优化落地后,接下来探索浏览器渲染执行优化点,涉及到浏览器,必定会联想到网页解析过程,下图清晰的展现静态资源如何经过浏览器最终显示:
当dom元素变化会致使浏览器从新执行渲染树生成、绘制,咱们称之为重排重绘。
什么是重排?当 render tree 中的一部分(或所有)由于元素的规模尺寸,布局,隐藏等改变而须要从新构建。这就称为重排(回流)。每一个页面至少须要一次回流,就是在页面第一次加载的时候。
浏览器结构示意图:
能够看到浏览器有负责解析、渲染请求内容的渲染引擎,哪些动做会致使浏览器重排:
(1)增长或删除 DOM 节点;
(2)display:none(重排并重绘); visibility:hidden(重绘);
(3)移动页面中的元素;
(4)改变元素尺寸(宽、高、内外边距、边框等);
(5)用户改变窗口大小,滚动页面等;
(6)页面初始渲染;
(7)改变元素内容(文本或图片等)。
offsetTop, offsetLeft,... scrollTop, scrollLeft, ... clientTop, clientLeft, ... getComputedStyle() (currentStyle in IE)
这些属性都须要实时回馈给用户的几何属性或者是布局属性,浏览器不得不当即执行渲染队列中的“待处理变化”,并随之触发重排返回正确的值。
document.body.style.minWidth = '12OOpx' document.body.style.overflow = 'hidden' //获取某div的偏移量 document.querySelector('xxx').offsetTop
咱们优化活动代码执行逻辑,将上述直接操做 dom 的操做修改成 class 样式操做,减小加载过程当中重复的dom操做。
善用 Vue 组件生命周期,在合适的 hook 去初始化数据,操做dom,可以大幅提高加载体验。
在mounted 阶段,浏览器已经完成 dom 与 css 规则树的 render,并完成 render tree布局,这时候再去发送数据请求,会拉长请求时间和渲染周期,因此建议在beforeCreate中执行,以此达到预渲染和请求的并行进行。
咱们将活动初始化数据的动做放在 beforeCreate 阶段,并将对 dom 的操做和监听挂载在 mounted 中。
{ beforeCreate(){ fetch({ url: topicUrl, params: { //... } }).then(res=>{ //数据处理 //... }) }, mounted() { // global listener window.addEventListener('xxx'); // get dom element by refs this.$refs.xxx // get dom element use native api document.querySelector } }
对浏览器来讲,整个渲染流程还没有开始或者说准备开始,对 vue 来讲,实例还没有被初始化,data observer 和 event/watcher 也还未被调用,这个时候请求页面初始化数据时机是比较成熟的。
相比 Native 页面,H5 页面体验问题主要是:打开一个 H5 页面须要作一系列处理,会有一段白屏时间,体验糟糕。
白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
本次专题优化,咱们采用以下方式去减小白屏时间:
其中改造骨架的方式是一种成本低,效果很是卓越的方式,更进阶的方式有服务端直出等。因为悟空活动专题有快,灵的特色,配置改变需实时生效,因此前期咱们权衡方案利弊,采用骨架,直接渲染过渡效果的方案。
页面加载html后直接显示加载效果,在底版本andriod手机中,webwiew初始化过程会有一个高度切换过程,加载后出现Native的titleBar,致使过渡效果会产生位置移动场景。
为了解决该问题,咱们使用css3动画来实现过渡效果延迟出现,避免与webview初始化冲突。
animation: loading 1s linear 300ms infinite; ··· @keyframes loading{ from { opacity: 1; } to { opacity: 1; } }
这一现象能侧面反映出,loading出现基本于webview初始化同期进行,速度很快。为了解决loaidng瞬移的问题,咱们采用纯css3实现loading延迟出现,不与webview初始化冲突。
下述表格展现同一微组件和配置的活动在总体优化先后网站总体体验评分,评分来自PageSpeed Insights。
国内活动 |
优化前 |
优化后 |
首次绘制 |
2.8s |
1.3s |
速度指数 |
4s |
3.8s |
绘制耗时 |
12s |
2.3s |
综合得分(满分 100) |
44 |
90 |
海外活动 |
优化前 |
优化后 |
首次绘制 |
3.5s |
1.3s |
速度指数 |
5.6s |
3.3s |
绘制耗时 |
3.5s |
2.8s |
综合得分(满分 100) |
67 |
92 |
相同配置专题:
相同配置专题:
关于指标,业界有很是多的方案和数据:
基于活动的特色以及业务常关注点:咱们对页面白屏时间以及首次渲染时长以及一些个性化指标进行了收集,目的是统计活动专题加载时长,寻找优化空间。
静态资源的加载速度,能够利用 performance Timing API 取得
白屏时间:
白屏时间 = 开始渲染时间(首字节时间+HTML 下载完成时间)= responseStart - navigationStart
首次渲染时长 = 所有事件注册时长 = loadEventEnd - navigationStart
页面绘制时间=获取数据到加载结束 = loadEventEnd - fetchEnd(自行记录)
关于性能数据的上报方式,平台使用 sendBeacon 进行无阻塞性能数据上报
navigator.sendBeacon() 方法可用于经过HTTP将少许数据异步传输到 Web 服务器。
这个方法主要用于知足统计和诊断代码的须要,发送代码一般尝试在卸载(unload)文档以前向 web 服务器发送数据。
function stat() { navigator.sendBeacon('/path', analyticsData) }点击并拖拽以移动
sendBeacon 发出的是异步请求,请求做为浏览器任务执行,与当前页面脱钩。所以该方法不会阻塞页面加载流程,也不会延迟页面加载。
在上述探索的同时,咱们同时在进行专题 SSR 、秒开、CSR的方案探索,不断尝试提高 H5 体验的方式,追求卓越。
在笔者看来,性能优化不是一种手段,而是一种意识,开发者在实际开发过程当中须要创建意识,在各处细节上去保证用户体验。
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:Labs2020 联系