关注「索引目录」公众号,获取更多干货。
在船上(使用VSAT)测试旧项目的实际网络速度(~1KB/s)时录制的。
问题
网络速度极慢,即使是少量数据也需要很长时间才能加载完毕。
在这种速度下,我为普通互联网搭建的系统无法正常工作:
- 前端
加载速度非常慢。 - API 请求
耗时很长,有时甚至会失败。 - 用户交互感觉
反应迟钝、卡顿。
系统的大部分部分都假定网络速度正常。
我意识到我需要仔细考虑前端如何请求数据以及API 如何响应,这样即使速度只有 1 KB/s,系统仍然可以让人感到可用。
加快软件运行速度
在发现软件在 1 KB/s 的速度下运行吃力之后,我专注于改进影响速度和响应性的每一个环节
。 以下是我的改进方法:
前端
我使用Preact和Vite构建了前端,优先考虑快速加载和响应速度,即使在网络速度极慢的情况下也能保持响应。
1. 为什么选择 Preact?
在 1 KB/s 的网络连接下,每一 KB 都至关重要。使用像 React 这样的大型框架,即使是简单的页面也会感觉很卡顿。这就是我转而使用 Preact 的原因,它提供了与 React 类似的 API(hooks、JSX、context),但核心大小压缩后只有约 3 KB。
我的项目采用微前端架构,系统的每个部分都是独立构建和部署的。
这意味着每个应用都会打包自己的 JavaScript 运行时环境——而使用 React 时,这些运行时环境很快就会累积到数百 KB。
使用Preact,每个应用的开销可以减少到几 KB。
这不仅加快了系统整体加载速度,还降低了低端设备的内存占用,同时仍然保持了 React 式的开发体验。
比较捆绑包大小
为了了解框架的选择究竟有多重要,我做了一个简单的测试。
我创建了三个最简的“Hello World”应用程序,分别使用React、Next.js和Preact,所有应用程序都移除了 CSS、资源和图标。然后,我使用它们各自的分析器构建了每个项目:
Next.js与next/bundle-analyzer
React和Preactnonzzz/vite-bundle-analyzer
下面的结果显示了 Preact 的打包体积比 React 和 Next.js 小多少。
2. 高级前端优化
选择 Preact 让我从较小的起点开始,但这仅仅是个开始。
为了让应用在 1 KB/s 的网络环境下真正可用,我进行了多项架构和构建层面的优化。
2.1. 捆绑拆分(动态导入 + Suspense)
我没有发布一个大的 JavaScript 包,而是将应用程序拆分成更小的块,并仅加载所需的内容,使用动态导入import()、 Suspenselazy()和SuspenseSuspense 来协调加载和回退 UI。
import { h, Suspense } from 'preact';
import { lazy } from 'preact/compat';
const Heavy = lazy(() => import('./HeavyComponent'));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}> // shown while HeavyComponent loads
<Heavy />
</Suspense>
);
}
2.2. 服务器端渲染 (SSR)
服务端渲染 (SSR) 通过直接从服务器发送可渲染的 HTML,显著提升用户体验。
用户甚至可以在 JavaScript 加载完成之前就立即看到内容。
然而,这段 HTML 代码最初是静态的。
页面还需要下载并加载 JavaScript 才能实现交互。
这就是为什么客户端包的大小仍然很重要,而这正是 Preact 的优势所在。
简而言之:SSR 提供即时内容,而 Preact 使其更快地实现交互。
2.3 资源优化与缓存
即使采用了服务器端渲染 (SSR) 和小型资源包,网络通常仍然是系统中速度最慢的部分。
为了最大限度地缩短加载时间,我专注于减小资源文件大小并提高浏览器缓存的效率。
所有静态资源,包括 JavaScript、CSS 和图片,都使用哈希文件名(例如 main.[hash].js)进行部署,以便浏览器可以无限期地缓存它们。
当部署新版本时,只会重新下载已更改的文件。
资源通过CDN提供服务,CDN 从最近的边缘位置交付资源,大大改善延迟,这在连接速度慢或不稳定的情况下尤为重要。
每项资产均配备:
Cache-Control: public, max-age=31536000, immutable
同时,HTML 会被轻度缓存,以便用户始终获得最新版本。
图片被转换为WebP 格式以减小文件大小,且不会造成明显的质量损失;同时使用占位符进行延迟加载,使内容能够立即显示。
后端
即使前端非常轻量级,如果 API 负载过重,应用运行速度仍然会很慢。
为了优化速度和负载大小,我重点关注了以下几个关键方面:
1. Zstd 压缩:
后端使用Zstd 压缩,压缩级别设为21,以实现最大程度的压缩,但仅在客户端请求时才会启用。
响应中包含一个标头,用于指示压缩方法(Content-Encoding: zstd),因此不支持 Zstd 的客户端仍会收到纯文本数据。
这在保持兼容性的同时,显著减小了有效负载的大小。
例如:客户端发送以下请求:
GET /api/data HTTP/1.1
Host: example.com
Accept-Encoding: gzip, zstd
标Accept-Encoding头告诉服务器客户端可以接受 gzip 或 zstd 格式的压缩数据,并希望服务器尽可能发送压缩数据。
示例:服务器发送以下响应:
HTTP/1.1 200 OK
Content-Encoding: zstd
Content-Length: 12345
<compressed data in zstd>
2. 使用 Protobuf 进行二进制序列化为了最大限度地减小有效负载大小并加快解析速度,API 响应使用Protobuf
而非 JSON 进行编码。Protobuf格式紧凑、速度快且易于维护,因此非常适合低带宽场景。
我还考虑了其他方案:
- JSON
:人类可读,但冗长且解析速度较慢。 - FlatBuffers
:非常紧凑快速,但维护起来很复杂,而且随着时间的推移更难发展。
Protobuf恰到好处:有效载荷小、解析速度快、模式可维护性强。
syntax = "proto3";
message User {
string name = 1;
repeated Address address = 2;
}
message Address {
string line = 1;
}
有关实际性能基准,请参阅protobuf.js 性能。
3. HTTP/3 传输
所有 API 请求均使用HTTP/3,充分利用了QUIC 的多路复用流和改进的丢包恢复机制。
与 HTTP/2 或 HTTP/1.1 相比,HTTP/3 降低了延迟,避免了队头阻塞,并能更有效地处理丢包,这在不稳定或速度较慢的网络上尤为有利。
概括
针对慢速网络进行优化需要关注技术栈的每一层,从前端渲染到 API 传输。
前端优化:
- Preact + Vite
,体积小巧,快速补水 -
使用 lazy()+进行包拆分,Suspense实现组件级和路由级延迟加载 - 使用SSR
可以即时生成 HTML,Preact 的小型运行时环境使其水合速度比 Next.js 快得多。 -
利用哈希文件名、CDN分发和WebP图像进行资源优化和缓存
API优化:
-
为最大限度减少有效载荷,有条件地应用Zstd 压缩(21 级)。 - Protobuf
是一种紧凑、快速且易于维护的序列化方法。 - HTTP/3 传输协议
可在不稳定的网络上实现低延迟、可靠的数据传输
这些策略结合起来,使得该应用程序即使在 1 KB/s 的速度下也能保持快速、响应迅速和可用,同时将总有效载荷保持在最低水平。
关注「索引目录」公众号,获取更多干货。

