前言
用户体验中最关键、最基础的部分是「速度」,任何精心设计的交互手段在极致的「速度」面前都会显得一文不值。然而用户设备的碎片化、移动端网络环境的多变、不断增加的业务代码都在拖慢用户端页面的加载速度。开发人员意识到页面加载有性能瓶颈后的第一想法往往是去重构代码,重构代码一定是解决性能问题最优先的手段吗?重构后的代码影响范围大,需要耗费开发、测试甚至产品非常多时间而且原本非常稳定的业务逻辑重构之后可能会变得非常脆弱,需要一定的迭代周期才能让重构代码再次变稳定。而重构优化后的最终效果也和开发人员的水平强关联,重构之后效果反而不如重构之前的例子也并不少见。然而性能优化当中网络相关的部分长期被开发人员忽视!
网络层的优化改动影响往往只限定在网络层,只需要保证向业务交付可靠数据即可,所以网络层非常方便进行单元测试,在保证网络层稳定的前提下会大幅减少了测试的工作量。除此之外,由于网络层是较为基础的工具组件,对网络层的优化能作用到所有业务提升整个应用的体验。
下面我根据个人的实践、理解和网上资料的整理了一些网络优化方法论以供参考。
网络请过程
在讨论「网络优化」之前先来回顾一下客户端完整网络请求的基本流程,在回顾过程中可以思考一下哪些节点是存在优化空间的。

当客户端通过 HTTPS 域名向服务端发起一个 HTTP 1.1 网络请求时的过程如下:
- 是解析域名(DNS),把域名解析成IP地址。其过程是向网络运营商的域名解析服务器请求域名对应的IP,如没有则向上一级域名服务器请求,直到根域名服务器。
- 根据域名解析得到的 IP 地址,通过三次握手建立 TCP 连接
- 建立 TCP 连接后进行 SSL 握手,客户端得到服务端的证书后校验证书
- 证书校验成功后将应用层发送的 请求 Path 连同 query 参数、 请求 header、请求 body 加密后打包成二制数据发送给连接层
- 客户端等待响应
- 客户端开始接收响应数据,解析响应 Header,根据 Header 里的压缩参数解压响应 Body
- 将解压后的数据回调给业务层
上面的流程只是一个概况,每个流程节点还有非常多的细节限于篇幅就不展开了。这些关键节点哪些是可以进行优化呢?在忽略细节的前提下可以提出以下几点疑问
- 网络请求的第一步是 DNS 解析,每次请求都要去解析一遍可以直接用 IP 地址发送请求省去 DNS 解析过程吗?
- 每次 TCP 连接都需要三次握手,可以用 UDP 替换 TCP 进行数据通信吗?
- 每次连接都要进行 SSL 握手,同一设备每次连接都要 SSL 握手似乎没必要
- 每次请求都要带上 Header 数据,而 Header 有可能是相同,相同的 Header 字段似乎没有必要再发送
- 请求与响应体在多数情况下都是 JSON,还有比 JSON 编码更高效的编码方式吗?
事实上针对以上疑问现阶段都有对应的解决方案,有些是需要在业务层解决,有些需要通过通信协议解决。下面会一一介绍每种解决方案的优缺点。当然除了通信协议流程节点优化之外,并发性能的限制也会对通信速度带来影响。这方面可以通过梳理更合理的业务请求顺序来优化,下面会提到。
IP 直连
前面提到的 DNS 解析,DNS 解析除了耗时与重复之外还存在其它问题:
- 部分DNS承载全网用户 40% 以上的查询请求,负载重,一旦故障,影响巨大
- 山寨、水货、刷ROM等移动设备的 LOCAL DNS 设置错误
- 运营商 DNS 域名劫持问题
- DNS污染、老化、脆弱
- IP 地址更新延迟
方案
客端户不再向运营商域名解析服务器请求解析,而是向自己的域名映射服务发送网络请求,服务端根据自定义策略返回当前用户或设备最优的IP地址列表。客户端解析IP地址列表从列表中找出最优IP地址并缓存结果,下次启动时优先使用缓存中的 IP 地址来发送请求,此时运营商的域名解析服务只是做为容灾备份。
直接用IP地址进行网络请求可以显著优化首次连接延时,在弱网情况下能提高单个请求的成功率。业界把此方案称为 HTTPDNS ,使用 HTTPDNS 方案仅需对网络接入层做改造,不会影响到业务层逻辑。
域名与 IP 映射策略没有固定范式,需要根据实际情况(开发成本、业务架构等)来决定最优方案。以下是业界一些参考样例:
- 就近原则,通过客户端 IP 地址所属地区按距离分配到不同的 IP 地址
- 最优 IP 检测,服务端返回可用 IP 地址例表,客户端轮询(Ping)不同 IP 地址找优 IP 缓存
- 统计法,开始时给用户随机分配 IP 地址,服务端统计客户端接口耗时。积累足够数据后分析不同地区最优 IP,下次客户端用最优 IP 访问
HTTPDNS 除了减小 DNS 耗时外,扩展使用范围还可以做到 A/B Test ,将负载均衡前置到客户端减小后端资源投入。目前头部云服务厂商都都提供了 HTTPDNS 的服务,但使用时仍然震要注意:
请求头 Host 字段:
如前所述发出的请求头部 host 字段变成 IP 地址,而服务器只认域名,导致无法定位单个 IP 后对应的服务(一个 IP 地址可能对应多个服务,每个服务的证书可能不一致)。

解决方案是在请求发出前把 host 字段还原为原始的域名即可,如下 host 字段 IP 地址替换之前的域名地址。

HTTPS 证书校验问题:
在 iOS 端域名替换为 IP 地址后会存在证书校验会失败,SSL 握手第三步是客户端收到服务端的证书后会进行两步较验:
- 校验证书信任链,查看证书是否为合法机构颁发
- 客户端会校验证书中包含的域名是否和当前请求域名一致,由于请求地址已被换成 IP 地址,故校验会失败
这种情况需要服务端返回正确的证书(正确的 host 字段),然后 hook 掉证书校验链,具体过程可以参考 阿里直连方案说明。
压缩数据
压缩网络传输数据能显著减少接口数据传输时长,节省带宽,在弱网情况下提高接口成功率。
传输 WebP 格式的图片
对于图片资源应在保证视觉效果的前提下用高压缩率格式去传输,WebP 格式的图片是一个不错的选择。
WebP 图片和 PNG 图片压缩率对比:
WebP 有损(肉眼无法看出差别)压缩可以达到PNG的原图的80%,即便是无损压缩在保留透明度情况下仍然比 PNG 无损压缩文件小 26%。
在相同图片质量的情况下 WebP 格式文件大小比 JPEG 格式的图片小 25-35%。WebP 意味着更小的数据传据量,值得注意的是相同大小的图片文件, WebP 在解压时比 PNG、JPEG 更耗时。
主流浏览器及操作系统都支持了 WebP 格式的图片,在 iOS 上稍差一些,Safari 直到 iOS 14.4 才支持。微信小程序 2.9.0 版本的 API 已支持 WebP image 标签展示。

使用 Protocol Buf 序列化结构化数据
目前主流的接口数据格式仍然还是 JSON, JSON 的优势是结构简单、可读性强,但冗余信息过多,解析时内存占用过大,传输时的压缩率也不如 Protobuf(在字符串数据占比不大情况下)。
Protobuf 主要通过字段编码与变长数字来提高数据压缩率。
字段编码
字段编码是指用固定的整型数字来与字符字段映射,编码后的数据字符字段将被对应的数字替换来节省空间,通过编译文件(.proto,可理解为编码映射表)可将数字还原成字符串字符。
变长数字
整型数字一般需要占用4个字节,但大多数情况下网络中传输的数字是都偏小的数字,用4个字节传输偏小的数字明显不合理。变长数字就是用一个字节的最高位当成标记位,如标记位有效则接下来一个字节是当前字节的一部分。这样原本需要4个字节的较小整数只需要占用1或者2个字节的空间,这样便节省了空间,提高了压缩率。
通过测试可以发现在相同数据情况下 Protobuf 消息体积减少了 34%, 官方的声称的解析速度是 JSON 的5倍。相对传统的 JSON 与 XML 格式数据 Protobuf 无论是解析速度还是数据压缩率都有着绝对的优势。并且 Protocbuf 支持 Java/Go/Objective-C/Swift/Dart/Javascript/C++ 等语言,Protobuf 已经是事实上的跨端、跨平台数据编码方式。
Protobuf 的缺点是可读性稍差,传输的数据需要对应的编译文件 (.proto) 才能解码,所以对抓包及网络层的拦截不太友好,但从另一方面讲 Protobuf 的编码方式在一定程度上提高了数据的安全性。
数据压缩方案
众所周知 HTTP 通过在请求头部添加 Accept-Encoding,响应头部添加 Content-Encoding 来指定数据传输编码方式,其类型有如下几种:
js
Content-Encoding: gzip // 最常用
Content-Encoding: compress // 基本被废弃
Content-Encoding: deflate
Content-Encoding: identity // 表示未经过压缩和修改
Content-Encoding: br // brotli
网络库通常默认使用 GZIP 进行编码,使用历史较长在不同平台都有不错的兼容性。brotli 是最近几年流行起来,是由 Google 推出的一种压缩率很高的压缩格式,其在浏览器上的兼容较差。
好在移动端上可以关闭默认的 GZIP 压缩,在应用层解决数据的压缩&解压缩。以 Flutter 为例,使用 brotli 解压代码如下:
dart
import 'dart:io';
import 'package:brotli/brotli.dart';
bool isBrotliEncoding(HttpHeaders header) {
return header.value(HttpHeaders.contentEncodingHeader)?.toLowerCase().contains('br') ?? false;
}
void main() async {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) => true;
final request = await client.get('host', 'port', 'replace/me');
var resp = await request.close();
if (isBrotliEncoding(resp.headers)) {
resp = resp
.cast<List<int>>()
// brotli 解压逻辑
.transform(brotli.decoder)
.map((b) => Uint8List.fromList(b));
}
final boyd = await resp.response.transform(utf8.decoder).join()
}
减少请求/响应头数据
在HTTP1.1协议中每次网络请求有请求和响应头,这部分的数据量在大多数情况下都是一样的,属于重复信息。请求&响应头是 HTPP1.1 协议规范,这部分的数据无法压缩省略只能通过协议层解决。HTTP2.0 协议在客户端和服务端都维护一个请求和响应头表,只有第一次请求会带上所有的数据,之后的请求只会带上变化的部分,没有变化请求响应头部信息会省略掉,减少头部信息即减少了网络请求的数据传输量。
升级协议
HTTP/2
HTPP/2(SPDY 演变而来)作为 HTTP/1.1 的更新版本,它解决了 HTTP/1.1 最極急解决的问题:
- 连接无法复用
- 队头阻塞
- Header 冗余数据多
HTTP/2 协议优点:
-
基于二进制分帧传输
-
- 流:流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2...N);
- 消息:是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。
- 帧:HTTP 2.0 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流,承载着特定类型的数据,如 HTTP 首部、负荷,等等
-
多路复用
-
- 同域名下所有通信都在单个连接上完成。
- 单个连接可以承载任意数量的双向数据流。
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
-
好处:
-
- 同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应,消除了因多个 TCP 连接而带来的延时和内存消耗。
- 并行交错地发送多个请求,请求之间互不影响。
- 并行交错地发送多个响应,响应之间互不干扰。
- 在 HTTP/2 中,每个请求都可以带一个 31bit 的优先值,0 表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧
-
头部压缩
客户端和服务端都维护一个请求和响应头表,只有第一次请求会带上所有的数据,之后的请求只会带上变化的部分,没有变化头部信息会省略掉。
- 服务端推送
客户端按序请求多个资源时,服务端接收到第一个请求后可提前推送后续资源到客户端,客户端网络层会缓存资源,等到请求资源时直接从缓存读取。典型的应用场景是打开 Web 页面时,服务端在返回 HTML 的同时推送 js/css 资源给客户端,客户端缓存资源文件,等解析完 HTML 后直接从缓存中读资源文件,省去了后续网络传输的过程
主流浏览器已全面支持 HTTP/2,可以通过这个测试地址测试你的服务端接口是否支持 HTTP/2
HTTP/3 (QUIC)

HTTP/3 是基于 Google 的 QUIC 协议标准化而来,不同于 HTTP/2 补丁式的更新,HTTP3 基于 UDP 协议进行重新开发,在 UDP 协议的基础上实现了滑动窗口、拥塞控制、应答确认等机制来保证数据可靠性并在协议层实现了以下功能:
- 0-RTT
- 多路复用
- 不存在 TCP 队头阻塞
- 连接迁移
- 基于UDP实现
- 免重复 SSL 握手 QCF
HTTP3 已正式发布,HTTP/2 需要操作系统网络库的支持而 QUIC 完全在应用层支持,因此它不依赖操作系统版本可随应用一同发布。业界大厂普遍都进行了 HTTP/3 的实践,阿里甚至自己实现了 HTTP/3 协议:XQUIC。对于中小企业可以利用开源 chromium 中的 cornet 组件(腾讯手Q、魔菇街),cornet 组件是用 C/C++ 开发的跨平台网络库,客户端可借助 cornet 的能力支持 QUIC,服务端 NGINX 2020 发布了支持 QUIC 的技术预览版,NGINX 官方已经正式支持 HTTP/3 与 QUIC。
对于 Flutter 应用来讲,由于官方网络库(dart:io)基于 socket 开发,目前还不支持 HTTP/3。但可以通过原生的能力让 Flutter 应用支持 HTTP3,官方已经封装好了对应的原生网络库,将请求转发到原生去从而充分利用原生网络库的能力。
请求管理
一般情况下开发人员只关心自己业务网络求请成功与否,否很少从全局去思考请求的顺序、优先级、并发数量等对整个项目的影响。下面根据我个人的思考和总节一些在业务层优化请求速度的方法:
- 针对不同网络情况、不同业务场景设定不同超时时长,确定合理的重传机制
- 整理不同业务请求的优先级,对于不紧急的业务延后请求,提高首屏响应速度
- 根据不同的网络环境确定不同的请求并发数量
- 域名收拢,减少域名数以尽量复用长链接
总结
以上就是关于网络优化方法的一些总结,由于时间仓促还是有一些遗漏。通过上面的总节其实可以发现,网络层优化相对简单,升级协议甚至什么都不需要改就可以达到优化的目的,享受技术发展宏利。