如果你在玩《剑网3缘起》,你会发现游戏有一个内置社区。虽然这个应用是个 CSR,但是会发现加载并不慢
而且这些都不是事后优化。所以正好可以分享一些可能对其产生作用的习惯和做法,或者说开发时可以注意的地方
TL;DR
总之就只有一个目标:尽量砍掉第一次加载的大小和尽量减少加载所需要花费的时间
- 减少加载体积
- 懒加载/延迟加载:利用 import/export 做代码分段
- 充分利用 http2 和 vite(开箱即用版 rollup )
- 非常巧合的做法,指跟 React Beta 版文档异曲同工
减少加载体积
前端的静态资源都在依赖网络传输,所以在第一屏尽量加载最小的资源。
使用 preact
这个应用在建立的时候 react 版本是 17,虽然已经有 react 18 但还不 stable。考虑一番之后觉得没有并发和批处理的 react 不够有吸引力。与此同时,preact 10 有:
- 更小的体积,即使带着
compact
最后也就不到 10k,保守预估 - 更快的运行
- react 16 兼容,沉淀组件直接用不用考虑需不需要升级或适配
<Suspense>
在当时已经稳定
当时的考虑是:现在用 preact 以后迁移回 react 18 问题也不是很大,所以在 react 18 没有稳定之前都可以用 preact。只不过在 TypeScript
环境下,写类型稍微痛苦了一些 —— 毕竟只有 api
是兼容的,类型可不兼容
JS 加载
首先是编译期处理,举个例子
// use-noop.ts
export function useNoop() {
return () => {}
}
// page.ts
import { useNoop } from './use-noop'
首先我们的规范决定了:工具放在 utils
目录中且一个文件仅有一个函数,并通过 export
导出
所以我们在最开始就解决了一个问题:尽可能拆掉首包体积,或是能自然的利用 ES Module Tree Shaking (Rollup)
接着,由于我们的目标是Chromium(cef) 76 往上,那么可以直接使用 module 的形式加载且不需要 polyfill
懒加载
懒加载内容分两个:
- 原本一起打包但已经 split 的 JS 文件
- 从网络上加载的媒体资源(图片,视频)
媒体资源懒加载
这里主要是图片,虽然 Chrome 已经有浏览器级别的懒加载(loading=“lazy”)
Chrome 会根据当前网络环境决定要加载多远的图片,不过 Chrome 的阈值有点捉摸不透。最后还是决定使用 lazysizes
,并且自己控制加载距离 —— 大概就在屏幕外面多一点点
于是「不必要」的网络请求又节省下来一点
JS 懒加载
可以懒加载的 JS 不少,对于我们来说大概有几种:
- 首屏用不到的模块,懒加载
- 路由,组件懒加载
首屏用不到的,或者在生产中用不到的模组例如 vconsole
、hls.js
,在第一次不参与加载
其次,我们在架构组织上采用分层组织,即页面组件(pages)作为消费层,业务代码根据需求和 feature
一一关联
我愿称之为渐进式 DDD,因为就是从 DDD 的思想不断简化,再通过磨合时间和程度逐步加上去,最后都习惯并理解「为什么持续维护项目型需要 DDD」
有机会的话下一篇分享 DDD 在我们实际场景中的运用。这里推荐一个项目叫 remesh,比较可惜的是这个框架诞生于我们的决定之后
# 这里隐藏掉了部分细节
- common
- component
- model
- repository
- service
- [feature]
- component
- model
- repository
- service
...
- pages
- [page]
这样,page
对于我们来说只是一个组合功能的载体,从而又带来一个好处:一个页面可以决定哪部分直接添加,哪部分进 lazyload。加上 preact
已经稳定的<Suspense>
,就可以实现不那么生硬的体验效果
并且所有的 page
都是通过路由 lazyload 的,也就是第一次加载不会加载到别的页面的 JS(这好像已经变成了基操)
到这其实有个问题:以上关于一个页面拆分懒加载,但它们始终都要在第一屏加载,下载量可是不变的
所以还有下一步操作,大小尽量砍了,该想怎么弄快了
利用 http2
http2 没有改动 http 的语义,但改动了头部压缩,新增优先级排序和多路复用
在 http1.1,由于每个请求都会开一个连接,所以早期前端优化有这么一手:把静态资源分布在不同域名下,例如 img1.example.com,img2.example.com。其实现在观察 github 也能看到类似的做法
但因为 http2 有了二进制分帧,所以基于此实现了共享TCP,到位了再根据标记和头部重新组装报文。在这之前 http1.1 是一整块纯文本,分区用的换行符(指头部和身体)
其实以上都「不重要」,只需要知道现在同一CDN下载不阻塞了。所以在 h2 之下,静态资源可以同时加载。这就解答了上面关于页面拆分的问题
异曲同工?
Dan Abramov 也就是 react 的创作者,被小右也就是 vue 的创作者大狙点头之后,对 react beta 文档爆更了 3 天。原因就是被吐槽 TTI 和 TBT 占用时间太长被 vue 的文档吊锤
所以他对 react beta 文档改动了什么?
- 工具函数拆分,改成一文件一函数
- 移除过时浏览器支持,从而移除
polyfill
的加载(react 现在也仅支持现代浏览器) - 懒加载资源,例如
@codemirror/lint
和eslint
所以现代前端优化,好像基础版就这么些操作,大家都是一样的。首先尽可能压低大小(可仅参考 gzip 之后大小),然后尽可能抬高加载速度显示速度(SSR,CDN)
实在不行,发现瓶颈出现在远端数据,那就上缓存!资源慢缓存资源,数据接口慢缓存数据,什么 service worker 全给它叠起来(x