如果你在玩《剑网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 不少,对于我们来说大概有几种:

  • 首屏用不到的模块,懒加载
  • 路由,组件懒加载

首屏用不到的,或者在生产中用不到的模组例如 vconsolehls.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.comimg2.example.com。其实现在观察 github 也能看到类似的做法

但因为 http2 有了二进制分帧,所以基于此实现了共享TCP,到位了再根据标记和头部重新组装报文。在这之前 http1.1 是一整块纯文本,分区用的换行符(指头部和身体)

其实以上都「不重要」,只需要知道现在同一CDN下载不阻塞了。所以在 h2 之下,静态资源可以同时加载。这就解答了上面关于页面拆分的问题

异曲同工?

Dan Abramov 也就是 react 的创作者,被小右也就是 vue 的创作者大狙点头之后,对 react beta 文档爆更了 3 天。原因就是被吐槽 TTI 和 TBT 占用时间太长被 vue 的文档吊锤

所以他对 react beta 文档改动了什么?

  • 工具函数拆分,改成一文件一函数
  • 移除过时浏览器支持,从而移除 polyfill 的加载(react 现在也仅支持现代浏览器)
  • 懒加载资源,例如 @codemirror/linteslint

所以现代前端优化,好像基础版就这么些操作,大家都是一样的。首先尽可能压低大小(可仅参考 gzip 之后大小),然后尽可能抬高加载速度显示速度(SSR,CDN)

实在不行,发现瓶颈出现在远端数据,那就上缓存!资源慢缓存资源,数据接口慢缓存数据,什么 service worker 全给它叠起来(x