移动网络优化实践
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移动网络优化实践相关的知识,希望对你有一定的参考价值。
参考技术A 网络优化对于App产品的用户体验至关重要,与公司的运营和营收息息相关。这里列举两个公开的数据:“ 页面加载超过3秒,57%的用户会离开。 ”
“ Amazon页面加载延长1秒,一年就会减少16亿美金营收。 ”
首先是网络不可用的问题。主要由以下几种原因导致:
GFW的拦截,原因你懂的。
DNS的劫持,端口的意外封禁等。
偏远地区网络基础设施比较差。
其次是网络加载时间长。原因包括: * 移动设备出于省电的目的,发出网络请求前需要先预热通信芯片。 * 网络请求需要跨网络运营商,物理路径长。 * HTTP请求是基于Socket设计的,请求发起之前会经历三次握手,断开时又会进行四次挥手。
最后是HTTP协议的数据安全问题。原因有: * HTTP协议的数据容易被抓包。Post包体数据经过加密能够避免泄露,但协议中的URL和header部分还是会暴露给抓包软件。HTTPS也面临相似的问题。 * 运营商数据恶意篡改严重。如下图中,App的网页中就被运营商插入了广告。
3
面对上述网络问题,我们首先在HTTP短连请求中进行了一些优化尝试。
1. 告别 DNS,直接使用 IP 地址
如果是首次发送基于 HTTP 协议的网路服务,第一件事就是进行 DNS 域名解析,我们统计过 DNS 解析成功率只有 98%,剩下 2% 是解析失败或者运营商 DNS 劫持(Local DNS 返回了非源站 IP 地址),同时 DNS 解析在 3G 下耗时 200 毫秒左右,4G 也有 100 毫秒左右,延迟明显。我们基于 TCP 连接,直接跳过了 DNS 解析阶段,使用内置 IP 列表的方式进行网络连接。
App 内置了一组 Server IP 列表,同时每个 IP 具备权重。每次建立新连接,会选择权重最高的 IP 地址进行连接。App 启动时,IP 列表的所有权重是相同的,此时会启动一组 Ping 的操作,根据 Ping 值的延迟时间来计算 IP 的权重,这么做的原理是 Ping 值越小的 IP 地址,连接后的网络传输延迟也应该相对更小。业界也有使用 HTTP DNS 方式来解决 DNS 劫持问题,同时返回最合适用户网络的 Server IP。然而 HTTP DNS 的开发和部署需要不小的开发成本,我们目前没有使用。
内置 Server IP 列表也会被更新,每次 App 启动后会有个 Mobile Config 服务(支持 TCP 和 HTTP 两种网络类型服务)更新 Server IP 列表,同时支持不同产品线的 Server IP 列表更新。因此,传统 DNS 解析能够解决多 IDC 导流的功能也可以通过此方法解决。
2. Socket 连接优化,减少连接时间
和 HTTP 协议中的 Keepalive 特性一样,最直接减少网络服务时间的优化手段就是保持长连接。每次 TCP 三次握手连接需要耗费客户端和服务端各一个 RTT(Round trip time)时间才能完成,就意味着 100-300 毫秒的延迟;TCP 协议自身应对网络拥塞的 Slow Start 机制也会影响新连接的传输性能。
App 使用了长连接池的方式来使用长连接,长连接池中维护了多个保持和服务端的 TCP 连接,每次网络服务发起后会从长连接池中获取一个空闲长连接,完成网络服务后再将该 TCP 连接放回长连接池。我们没有在单个 TCP 连接上实现 Pipeline 和 Multiplexing 机制,而是采用最简单的 FIFO 机制,原因有二:
简化 Mobile Gateway 的服务处理逻辑,减少开发成本;
在服务端同时返回多个响应时,如果某个响应报文非常大,使用多个长连接方式可以加快接收服务响应报文速度。
如果发起网络服务时长连接池中的 TCP 连接都正在被占用,或者 TCP 长连接的网络服务失败,则会发起一个 TCP 短连接实现网络服务。这里长连接和短连接的区别仅仅是服务完成后是否直接关闭这个 TCP 连接。
附: Pipeline 和 Multiplexing 是有区别的,如 HTTP/1.1 支持 Pipeline,客户端能否同时发送多个请求,但是服务端返回响应时也要按照请求的发送次序来返回响应;SPDY 和 HTTP/2 协议支持 Multiplexing,即支持响应报文的乱序返回,发送请求和接收响应互不干扰,因此避免了 HTTP/1.1 Pipeline 也没能完全解决的 Head of line blocking 问题。
3. 弱网和网络抖动优化
App 引入了网络质量参数,通过网络类型和端到端 Ping 值进行计算,根据不同的网络质量改变网络服务策略:
调整长连接池个数:例如在 2G/2.5G Egde 网络下,会减少长连接池个数为 1(运营商会限制单个目标 IP 的 TCP 连接个数);WIFI 网络下可以增加长连接池个数等机制。
动态调整 TCP connection、write、read 的超时时间。
网络类型切换时,例如 WIFI 和移动网络、4G/3G 切换至 2G 时,客户端 IP 地址会发生变化,已经连接上的 TCP Socket 注定已经失效(每个 Socket 对应一个四元组:源 IP、源 Port、目标 IP、目标 Port),此时会自动关闭所有空闲长连接,现有网络服务也会根据状态自动重试。
4. 数据格式优化,减少数据传输量和序列化时间
传输数据量越小,在相同 TCP 连接上的传输时间越短。携程 App 曾经使用自行设计的一套数据格式,后来和 Google ProtocolBuffer 对比后发现,特定数据类型下数据包大小会降低 20-30%,序列化和反序列化时间可以降低 10-20%,因此目前核心服务都在逐步迁移到到 ProtocolBuffer 格式。另外 Facebook 曾分享过他们使用 FlatBuffer 数据格式 提高性能的实践,我们分析后不太适合携程的业务场景因而没有使用。
5. 引入重试机制,提升网络服务成功率
受 TCP 协议重传机制来保证可靠传输的机制启发,我们在应用层面也引入了重试机制来提高网络服务成功率。我们发现 90% 以上的的网络服务失败都是由于网络连接失败,此时再次重试是有机会连接成功并完成服务的;同时我们发现前面提到的网络服务生命周期处于 1 建立连接、序列化网络请求报文、发送网络请求这三个阶段失败时,都是可以自动重试的,因为我们可以确信请求还没有达到服务端进行处理,不会产生幂等性问题(如果存在幂等性问题,会出现重复订单等情况)。当网络服务需要重试时,会使用短连接进行补偿,而不再使用长连接。
实现了上述机制后,携程 App 网络服务成功率由原先的 95.3%+ 提升为如今的 99.5%+(这里的服务成功率是指端到端服务成功率,即客户端采集的服务成功数除以请求总量计算的,并且不区分当前网络状况),效果显著。
6. 其他网络服务机制 & Tricks
携程 App 也实现了其他一些网络服务机制方便业务开发,如网络服务优先级机制,高优先级服务优先使用长连接,低优先级服务默认使用短连接;网络服务依赖机制,根据依赖关系自动发起或取消网络服务,例如主服务失败时,子服务自动取消。
开发过程中我们也发现一些移动平台上的 TCP Socket 开发 tricks:
ios 平台上的原生 Socket 接口创建连接并不会激活移动网络,这里原生 Socket 接口是指 POSIX Socket 接口,必须使用 CFSocket 或者再上层的网络接口尝试网络连接时才会激活网络。因此携程 App 启动时会优先激活注册一些第三方 SDK 以及发送 HTTP 请求来激活移动网络。
合理设置 Socket 的几个参数:SO_KEEPALIVE 参数确保 TCP 连接保持(注:此 KeepAlive 是 TCP 中的属性,和 HTTP 的 KeepAlive 是两个场景概念),SO_NOSIGPIPE 参数关闭 SIGPIPE 事件,TCP_NODELAY 参数关闭 TCP Nagle 算法的影响。
由于 iOS 要求支持 IPv6-Only 网络,因此使用原生 Socket 必须支持 IPv6。
如果使用 select 来处理 nonblocking IO 操作,确保正确处理不同的返回值和超时参数。
保持 TCP 长连接可用性的心跳机制:对于非 IM 类应用而言,心跳机制的作用不大,因为用户会不断触发请求去使用 TCP 连接,尤其在携程业务场景下,通过数据统计发现使用心跳与否对服务耗时和成功率影响极小,因此目前已经关闭心跳机制。原先的心跳机制是 TCP 长连接池中的空闲 TCP 连接每 60 秒发送一个心跳包到 Gateway,Gateway 返回一个心跳响应包,从而让双方确认 TCP 连接有效。
Hybrid 网络服务优化
携程 App 中有相当比例的业务是使用 Hybrid 技术实现的,运行在 WebView 环境中,其中的所有网络服务(HTTP 请求)都是由系统控制的,我们无法掌控,也就无法进行优化,其端到端服务成功率也仅有 97% 左右(注:这里指页面中业务逻辑发送的网络服务请求,而非静态资源请求)。
我们采用了名为『TCP Tunnel for Hybrid』的技术方案来优化 Hybrid 网络服务,和传统 HTTP 加速产品的方法不同,我们没有采用拦截 HTTP 请求再转发的方式,而是在携程 Hybrid 框架中的网络服务层进行自动切换。
如图所示,该技术方案的流程如下:
如果 App 支持 TCP Tunnel for Hybrid,Hybrid 业务在发网络服务时,会通过 Hybrid 接口转发至 App Native 层的 TCP 网络通讯层,该模块会封装这个 HTTP 请求,作为 TCP 网络服务的 Payload 转发到 TCP Gateway;
TCP Gateway 会根据服务号判断出是 Hybrid 转发服务,解包后将 Payload 直接转发至 HTTP Gateway,此 HTTP 请求对 HTTP Gateway 是透明的,HTTP Gateway 无需区分是 App 直接发来的还是 TCP Gateway 转发来的 HTTP 请求;
后端业务服务处理完成后,HTTP 响应会经 HTTP Gateway 返回给 TCP Gateway,TCP Gateway 将此 HTTP 响应作为 Payload 返回给 App 的 TCP 网络通讯层;
TCP 网络通讯层会再将该 Payload 反序列化后返回给 Hybrid 框架,最终异步回调给 Hybrid 业务调用方。整个过程对于 Hybrid 业务调用方也是透明的,它并不知道 TCP Tunnel 的存在。
采用该技术方案后,携程 App 中 Hybrid 业务的网络服务成功率提升至 99% 以上,平均耗时下降了 30%。
海外网络服务优化
携程目前没有部署海外 IDC,海外用户在使用 App 时需要访问位于国内的 IDC,服务平均耗时明显高于国内用户。我们采用了名为『TCP Bypass for Oversea』的技术方案来优化海外网络服务性能,主要是使用了 Akamai 的海外专属网络通道,同时在携程国内 IDC 部署了局端设备,使用专用加速通道的方式来提升海外用户体验。
海外用户启动 App 后先通过 Akamai 定制域名获取 Server IP,所有网络服务优先走 Akamai 通道;如果 Akamai 通道的网络服务失败并且重试机制生效时,会改走传统 Internet 通道进行重试。相比只用传统 Internet 通道,在保持网络服务成功率不变的情况下,使用 Akamai 通道 Bypass 技术后平均服务耗时下降了 33%。
其他网络协议探讨
过去两年我们的网络服务优化工作都是基于 TCP 协议实现的,基本达到了优化目标。不过这两年来新的应用层网络协议 SPDY 和 HTTP/2 逐步迈入主流,基于 UDP 的 QUIC 协议看起来也非常有趣,值得跟进调研。
SPDY & HTTP/2
SPDY 是 Google 基于 TCP 开发的网络应用层协议,目前已经停止开发,转向支持基于 SPDY 成果设计的 HTTP/2 协议,HTTP/2 协议的核心改进其实就是针对 HTTP/1.x 中影响延迟性能的痛点进行优化:
Header 压缩:压缩冗余的 HTTP 请求和响应 Header。
支持 Multiplexing:支持一个 TCP 连接上同时实现多个请求和响应。
保持长连接(比 HTTP/1.x 更彻底):减少网络连接时间。
支持推送:可以由服务端主动推送数据到客户端。
官方性能测试结果显示使用 SPDY 或者 HTTP/2 的页面加载时间减少 30% 左右,不过这是针对网页的测试结果,对于 App 中的网络服务,具体优化效果我们还在进行内部测试,不过其优化手段看和目前我们使用 TCP 协议的优化手段类似,因此性能优化效果可能不会很显著。
QUIC
QUIC 是 Google 基于 UDP 开发的应用层协议,UDP 协议无需连接,不存在重传机制,因此应用层需要保证服务的可靠性。目前国内腾讯有针对弱网络尝试过 QUIC 协议,我们也在进行测试,最终是否会采用还需要看测试的结果。
综述
技术只是手段,最终还是要反映在业务效果上。我们已经实现除静态资源等需要访问 CDN 的网络请求外,其他 App 网络服务使用统一的 TCP 通道,从而具备更好的性能调优和业务监控能力。携程目前基于 TCP 协议的各种 App 网络服务优化,也是各种技术方案的平衡,虽然目前 HTTP/2 等新协议逐步成熟,但是 TCP 协议自身的灵活性支持有针对性的性能优化,还是具备其特别的优势,希望我们的实践总结能对国内无线技术从业者有一些借鉴价值。
有货移动WEB端性能优化探索实践
在移动互联网的时代里,对于一个web站点来说,移动端的用户体验尤为重要。现代web站点的设计和开发都是以移动优先作为第一原则,我们也专门为了移动端的web站点做了相应的优化和提升。而网页的打开速度和页面的流畅度,对于用户是否长时间访问至关重要。我们在移动端的站点通过一系列的方法,最终为了快速打开页面展示网页内容,触达用户,同时能流畅的浏览网页。
移动端的硬件条件,网络条件相对于桌面端,会复杂的多,设备类型多样,硬件配置参差不齐,分辨率碎片化,网络状况在移动过程中稳定性,速率都会变化,而对于一个页面到达用户的终端展示,会经过,用户发起请求,服务端接受请求,服务端处理请求,返回响应内容,在用户终端的浏览器展示内容,用户操作页面发起其他页面时间,而这个过程中任何一个环节的延迟都会造成性能瓶颈,降低用户继续访问的可能性,所以我们在服务器端,浏览器端,网络加载,多个方面做了一系列的优化工作。
WEB服务端优化
有货的WEB端主要使用了nodejs,基于后端服务提供的HTTP接口服务来实现的前后端分离,这里的服务端优化主要是指在nodejs实现的web服务端进行优化。
优化的目的是提升服务端的响应和并发能力,充分发挥nodejs的异步非阻塞的特性,主要从以下几个方面去优化。
接口服务调用的优化
对于一个页面展示的路由,要处理这个路由,可能需要调用多个接口并且进行进行界面逻辑的处理。大体过程:
· 接口合并 我们对于一个页面调用可以合并的接口,进行接口合并,减少接口调用次数,如:以商品详情页为例,商品的一些特性,可以在一个接口返回,尽可能的减少接口调用的个数,因为每次接口的处理都有网络IO,对象序列化,压缩和解压的过程。
· 接口异步调用 但是并不是所有的接口都可以合并,对于无法合并的接口,我们尽量使用node的异步非阻塞的特性,进行异步调用,同时调取多个接口,而最终的调用耗时取决于最慢的接口。
这里要说明一点:对于接口依赖,如A接口依赖B接口的返回结果,像这种情况,我们最好梳理下接口设计,减少这样的串行调用,因为这样,调用耗时是多个接口耗时的总和。
· 减少接口交互数据 返回的数据较多的情况下,会导致JSON序列化,数据批量对象处理,产生额外的性能损耗。可以做下接口返回数据结构的精简,返回必要的字段(页面会展示用到的数据)以及可以调整返回item个数。从而达到减少数据的返回消息体的大小。此外请求接口时需要gzip压缩,可以大大的减少网络传输的时间,尽管需要解压会消耗一部分CPU的时间,但是对接网络IO的损耗,还是值得的。
可以分享一组基准压测数据,在调用接口使用gzip和不使用gzip的QPS数据
constrq =
require(
'request-promise');
constcallApi =
async(isGzip,times) => {
let
index =
0;
const
timeFlag = process.hrtime();
for
(
leti =
0; i < times; i++) {
await
rq({
url:
'http://xxx.xxx.xxx.xxx/top250.json',
//替换成内网的http接口
gzip: isGzip
}).then(data => {
index++;
});
}
const
duration = process.hrtime(timeFlag);
console
.log(
`gzip ${isGzip}:${Math.round((duration[0] * 1e9 + duration[1]) / 10000) / 100}ms`);
console
.log(
`result:${index}`);
Promise
.resolve(
'done');
};
(
async()=>{
await
callApi(
false,
100);
await
callApi(
true,
100);
await
callApi(
false,
1000);
await
callApi(
true,
1000);
})();
gzip false:821.74ms
result:100
gzip true:581.83ms
result:100
gzip false:9168.54ms
result:1000
gzip true:5846.5ms
result:1000
· 减少接口调用次数 如何减少接口调用次数呢?对于一个页面,可能就会存在调用必要的接口,在这里我们使用到了缓存机制,对于热数据进行接口缓存,我们使用了一些内存数据库,同时对于一些规格数据可以进行进程级的缓存(如:导航信息,品类信息等)。缓存是有一定原则的:第一,需要容易命中的数据,第二,可以被缓存的数据,数据更新频率是可控的。通过缓存机制,一部分接口调用就会走到缓存,减少的接口调用的IO。此外缓存的数据可以是对接口数据处理后的视图对象,同时也减少的数据处理的时间。
· 内部服务调用DNS缓存 我们的内部服务使用域名方式,为了提高服务的灵活配置,但是需要内部DNS服务器进行域名解析,这个是有一定耗时的,所以我们在DNS这块加了DNS应用端cache,减少DNS解析的时间。
业务处理的优化
现在我们主要的服务端业务处理,主要对于页面逻辑的处理,如路由控制,会话处理,视图对象处理,模板渲染。我们在这些处理过程中进行了一些优化。
如何发现node的性能问题,主要可以使用cpu-profile进行cpu处理堆栈的抓取,然后使用chrome的dev-tools进行火焰图的分析,找到性能瓶颈。
· 计算密集型操作使用原生实现 js是不擅长计算密集型的操作,如Hash处理,加密解密,压缩解压,像这些操作可以直接使用nodejs提供的原生实现(crypto, Zlib)
以下是一组使用原生和js的md5处理性能对比:
constmd5js =
require(
'md5.js');
constcrypto =
require(
'crypto');
consttestText =
'中文qwerttyuuuioopp[[]ddffgasjdkaskldadasssssssssdsadacasfadsd;a';
const testMd5 = (title, text, func) => {
let
ret;
const
timeFlag = process.hrtime();
for
(
leti =
0; i <
10000; i++) {
ret = func(text);
}
const
duration = process.hrtime(timeFlag);
console
.log(
`${title}:${Math.round((duration[0] * 1e9 + duration[1]) / 10000) / 100}`);
console
.log(
`result:${ret}`);
}
testMd5(
'native md5:', testText, words => {
return
crypto.createHash(
'md5').update(words,
'utf8').digest(
'hex');
});
testMd5(
'js md5:', testText, words => {
returnnew
md5js().update(words,
'utf8').digest(
'hex');
});
返回结果:
native md5::31.59
result:5d3b7d53fdd4daaa2d75370e8a5d1789
js md5::181.54
result:5d3b7d53fdd4daaa2d75370e8a5d1789
差距还是比较明显的。
· 模板渲染的优化 我们在实际使用过程中,发现模板的渲染是十分消耗性能的,特别的模板的预处理过程,如果预处理过程是在用户访问过程中去处理,会慢不止一个数量级,所以我们把预处理的过程提前了(改造了hbs),在启动web应用时,已经预编译完成。同时我们发现handlebars的一些默认配置属性,如缩减处理,在字符串拼接过程中会损耗一定的性能,所以可以关闭html片段的缩减。
此外,我们还把可以缓存的html片段进行进程级的缓存,性能提升显著,可以把一些不怎么会变的html公共部分进行缓存。通过内部缓存刷新机制进行定时刷新html片段。
nginx的优化
· 启用page cache 使用了nginx的proxy_cache模块,配置了一些缓存机制,不同页面路由的缓存时长会读取node服务在http头里面返回的max-age时间。
proxy_cache cache_one_wap;
proxy_cache_valid 200 1m;
proxy_cache_min_uses 1;
proxy_cache_key $host$uri$args;
add_header X-Cache-Status $upstream_cache_status;
然后我们会在应用服务添加max-age配置的中间件,对路由进行拦截装饰http header:
const cachePage = {
'/'
: x * MINUTE,
'/boys'
: x * MINUTE,
'/girls'
: x * MINUTE,
'/kids'
: x * MINUTE,
'/lifestyle'
: x * MINUTE,
...
}
另外要注意一个设置nginx缓存的时候,如果有服务端设置cookies的情况下,并且以服务端cookies的值作为标识用户会话信息,不要设置proxy_ignore_headers "Set-Cookie";,不然缓存会导致会话信息窜读的情况。
· 全站HTTPS 为什么要上全站https呢,这个主要考虑到https可以防止中间人攻击以及内容劫持,提高网站的访问安全性。但是因为多了SSL/TLS的服务端和浏览器端的处理,在性能方面也会有相应的下降,但是我们同时也启用HTTP/2,而主要的特性:多路复用 (Multiplexing)多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。另外PWA里面的serviceworker 也是必须要求网站的协议是https的,同时也是为了这个做了铺垫,HTTPS是现代WEB的发展趋势。至于PWA不是本文讨论的重点,可以在后续其他的主题展开。
浏览器端优化
移动终端五花八门,导致过重的浏览器的处理和效果,会导致体验的不一致,特别是安卓手机,所以我们在浏览器端的策略是,尽量轻量化网页,当前页面只处理当前必要的内容多页面的方式。这个和现在google提出的开源项目AMP是一个思路。
首屏直出优化
从用户发出请求到页面完全展示,一般来说在网络正常基本上1s以上,但是如果页面打开耗时超过1s,用户流失的概率就会线性上升。所以移动端秒开至关重要,所以我们的思路是减少白屏时间,尽快把浏览器可视区域展示出来,就是所谓的首屏,我们就从以下几个方面做了优化。
· 直出文档,简化dom结构 服务端进行HTML渲染输出,只处理首屏需要的HTML,并且简化DOM的树状结构,如:
<div>
<ul>
<li>xxxx</li>
<li>xxxx</li>
</ul>
</div>
可以简化成
<ul>
<li>xxxx</li>
<li>xxxx</li>
</ul>
在页面只保留必要的DOM,此外,可以估算下世面手机分辨率,确定最大首屏的输出可视区域的DOM,如果需要滚屏到第二屏的,可以延迟通过Ajax获取内容加载。减少DOM,可以减少HTML的输出,当然更重要的浏览器的布局和渲染的时间大大减少。当然首屏的静态资源和样式要优先加载,下个关键点就是首屏只加载所需样式和静态资源。
· 首屏只加载所需样式和静态资源 光有DOM的处理是远远不够的,要从白屏到展示完整的首屏,还需要样式和静态资源。所以要优先加载样式和静态资源,所以直接把公共样式中首屏用到的样式抽离出来,并且首屏用到的样式,直接在html页面内置。此外,图片和字体等其他需要展示的部分,优先加载,促使首页快速展示出来。
· 首屏渲染,js延迟执行 当首屏渲染的时候,这时候js的执行可能会阻塞渲染的线程,所以为了减少对浏览器主线程的渲染过程,尽量延迟进行js执行,特别是操作DOM的情况,不然首屏展示过程中会产生额外的重布局和重绘,js引入或代码直接放到页面的底部,在body之后,在html之前。
· 优化直出服务端处理时间 另外再强调下直出的关键,服务端的处理wait尽量减少,对于首屏直出至关重要。
图片优化
· 图片质量和体积控制 对于移动终端来说,分辨率相当于桌面会小很多,首先会降低图片的分辨率,以及图片的DPI值,第二步会降低图片的质量,以保证图片的体积变小。
· 提高CDN缓存命中,第一,减少缩略图的尺寸规格,第二,尽量和其他端的图片规格保持一致,如APP端,小程序。
· WEBP的运用 webp不是所有的浏览器都支持,目前支持webp的浏览器,如下:
参见:https://caniuse.com/#feat=webp
所以,我们的做法是对于需要js加载的图片,进行webp的判断,如果支持,就是直接加载webp的格式图片,如果不支持,采用默认的jpg的格式。
if(
window.supportWebp && (
/format\/png/i.test(query) ||
/format\/jpg/i.test(query))) {
imgUrl = imgUrl.replace(
/format\/png/i,
'format/webp').replace(
/format\/jpg/i,
'format/webp');
}
浏览器端缓存优化
当存在缓存,可以减少浏览器的再次请求,大大提升了网页的打开速度,一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。那么下面我们就来看看服务器端缓存的原理。
· 缓存优化(max-age) 页面的缓存状态是由http协议的header决定的,我们主要使用了max-age(单位S),设置缓存的最长有效时间,使用的是时间长短,例如说我设置max-age=60,也就是说在请求发出后的60秒内,浏览器再次请求时不会再请求服务器,而是从浏览器缓存中读取数据。
· 预加载和懒加载 预加载和懒加载是一对好兄弟,用的好,可以极高提升浏览器端的体验,就是要确定在何时预加载,何时懒加载。我们主要在浏览器首屏结束后,当浏览器相对idle的时候,可以预加载下一屏即将展示的内容。当用户在即将触发下一屏时,下一屏的数据或DOM已经stay by了,自然体验会流畅很多,但是在预加载是需要一个度,因为一个页面的DOM过多,对于浏览器占有的内存也会过多,预加载最好是用户即将触发需要浏览的内容,如第二屏,轮播后面的内容,tab页等。懒加载的运用场景主要还是为了减少单次DOM渲染的大小,对于当前页面的非可视区域,当需要展示或用户事件触发才进行加载。所以懒加载和预加载,在不同的场景下会有不同的运用,前提是保障页面的流畅度。
· DNS预读取 DNS预读取配置的DNS的解析,可以减少DNS的次数,也可以加速不同域名的资源加载,目前支持的浏览器还是比较多的。
配置也很简单:
<link rel="dns-prefetch" href="//cdn.yoho.cn">
<link rel="dns-prefetch" href="//static.yohobuy.com">
<link rel="dns-prefetch" href="//img10.static.yhbimg.com">
<link rel="dns-prefetch" href="//img11.static.yhbimg.com">
<link rel="dns-prefetch" href="//img12.static.yhbimg.com">
<link rel="dns-prefetch" href="//img13.static.yhbimg.com">
<link rel="dns-prefetch" href="//analytics.m.yohobuy.com">
<link rel="dns-prefetch" href="//search.m.yohobuy.com">
<link rel="dns-prefetch" href="//list.m.yohobuy.com">
<link rel="dns-prefetch" href="//guang.m.yohobuy.com">
当然最好的减少DNS的时间是减少站点使用DNS的数量,我们会去掉部分二级域名。
CSS,JS的优化
项目构建主要采用了webpack的工具链,对css,进行依赖管理和构建打包,最小化css,js,并针对我们现有的多页面项目进行多入口的分包管理。
· 多页面css和js构建 打包代码如下,便利js的源文件目录,构建各个页面模块的js,所有的页面会包含两个js文件,libjs(全局公用的js),xxx.js(当前页面特有的js),css也是一样。这样每个页面的js和css都会最小化,同时我们也对这些个静态字符串文件进行gzip压缩,当然这些文件会按照版本进行静态存储,以及CDN的缓存。
// 构建各模块子页面JS
// 新的生成规则 module/page/index.js
shelljs.ls(path.join(__dirname,
'../js/**/index.js')).forEach((f) => {
const
dir = _.slice(f.split(
'/'), -
3);
// [modulename, page, index.js]
// Important
// 生成规则:module.page: './js/module/page/index.js'
entries[
`${dir[0]}.${dir[1]}`] = path.join(__dirname,
`../js/${dir.join('/')}`);
});
DOM优化
页面流畅度和DOM渲染和操作息息相关,渲染流程大致如下:
· 处理HTML 标记并构建 DOM 树。
· 处理 CSS 标记并构建 CSSOM 树。
· 将 DOM 与 CSSOM 合并成一个渲染树。
· 根据渲染树来布局,以计算每个节点的几何信息。
· 将各个节点绘制到屏幕上。
可以使用DEVTOOLS分析整个渲染过程中那块存在性能问题。
· 简化DOM,DOM操作优化 简化DOM可以减少渲染过程的时间,优化DOM操作,可以减少重布局和重绘的时间。简化DOM在上面的首屏直出已经介绍过相应的做法。这里主要说下DOM操作的优化,第一,减少DOM操作次数,可以把多次DOM操作在js的执行过程中生成好结果HTML,一次插入到DOM;第二,尽量在使用不在页面DOM树里面直接操作,可以脱离文档流的DOM中进行操作,可以使用fragment,一次插入文档流中。当然现在的react和vue都使用虚拟DOM的技术,通过diff算法进行通用化的DOM操作。这个也不失是一种效率高效的做法,但是对于一些不易优化的页面,还是需要人为干预和操作DOM使其性能最好。
· 减少重布局和重绘 第一,要减少布局调整,当您更改样式时,浏览器会检查任何更改是否需要计算布局,以及是否需要更新渲染树。对“几何属性”(如宽度、高度、左侧或顶部)的更改都需要布局计算。第二,绘制的复杂度、减小绘制区域:除 transform 或 opacity 属性之外,更改任何属性始终都会触发绘制。绘制通常是像素管道中开销最大的部分;应尽可能避免绘制。通过层的提升和动画的编排来减少绘制区域。可以使用 Chrome DevTools 来快速确定正在绘制的区域。打开 DevTools,按下键盘上的 Esc 键。在出现的面板中,转到“rendering”标签,然后选中“Show paint rectangles”。每次发生绘制时,Chrome 将让屏幕闪烁绿色。如果看到整个屏幕闪烁绿色,或看到你认为不应绘制的屏幕区域,则应当进一步研究。
页面动画优化 尽量使用CSS3的动画,使用transform 和 opacity 属性更改来实现动画。使用 will-change 或 translateZ 提升移动的元素。避免过度使用提升规则;各层都需要内存和管理开销。此外,需要减少动画的图层,每多一个图层,会多一份内存占有和管理的开销。如果一定要使用js的动画,建议使用:requestAnimationFrame。此外,能不用页面动画的场景尽量不要使用动画,如果一定要使用,可以简化动画渲染的过程,之前我们使用过一个js插件,iScroll就是一个案例,页面内初始化了多个iScroll实例,特别在安卓手机上特别卡顿,最后,我们的解决办法是自己使用css3动画和touch事件简单轻量的实现需要滑动的部分,对于页面滚动部分使用了原生的scroll,保证了不同终端体验一致。
移动web端的优化以上每个点如果展开去讲,都可以单独写一篇文章,我们分别在以上方面做了优化,并且,也产生了比较不错的效果,移动端的打开速度和体验都有了不错的提升,普遍打开的时间提升了30-50%,在网络稳定的情况下,基本上服务端的耗时在50ms以内,首屏时间在500ms以内,但是优化这件事情是永无止境的,没有最好,只有更好,需要开发者探究根本勇于创新,达到更好的更优的境地。
我们后续,还可以在web的基本技术点上深挖,同时在PWA以及AMP等现代web新思维的多个方面大家积极面对,继续探索,在未来的web前端之路可以走的更好,提供更优的用户体验,创造更高社会价值。
以上是关于移动网络优化实践的主要内容,如果未能解决你的问题,请参考以下文章