查看原文
其他

【第2287期】让 Instagram.com 变得更快: 缓存优先

Smith 前端早读课 2024-03-07

前言

本系列性能优化第三篇。今日前端早读课文章由京东@Smith翻译授权分享。

正文从这开始~~~

缓存优先

经过前面文章中所说的优化,向客户端推送数据的时间已经被尽可能地提前了。怎么才能让页面更快地获取到数据呢?唯一的思路就是不经由网络的请求和推送数据。我们可以采用缓存优先的渲染机制来实现,当然「缓存」也意味着在一定时间内用户会看到之前的页面数据。在这种实现中,一旦页面加载完成,我们会立即给用户展示之前 Feed 和 Story 的副本缓存,并且在新数据可用时覆盖旧的缓存。

instagram.com 是使用Redux来管理前端状态,因此我们的实现方式是,在更高层次上将一个Redux Store的子集存储到indexedDB的表中,然后在页面初始化渲染时将indexedDB缓存的数据状态 rehydrate 回store中。然而,由于 indexedDB 操作、请求服务端数据、用户交互都是异步的,无法判断它们的先后顺序。因此,当用户操作影响到缓存数据时,会遇到一个问题: 我们希望能确保这些对缓存数据的改动,也同样应用在服务端返回的数据。

译注: rehydrate 的直译是「注水」,感觉不是很准确,这里不做翻译。更多可以参考: react 中出现的"hydrate"这个单词到底是什么意思?

举个例子,如果就按原来的方式直接处理缓存的状态数据,我们会遇到这样的问题:首先,我们同时从缓存和网络加载数据,由于缓存优先的策略,因此缓存数据先渲染给用户。然后,用户对某个 feed 操作了「喜欢」。这之后一旦这个 feed 最新的网络请求返回时,它就会覆盖该 feed 的状态,被覆盖后该 feed 就不包含用户刚刚对 feed 操作的「喜欢」(请参见下图)。

为解决这个问题,我们需要一个新的方法,它在处理缓存状态的同时,还要缓存这些对状态的处理操作,然后在服务端返回的数据中重新执行一遍这些操作。如果你有 Git 或其它版本控制工具的经验,这看上去是不是很熟悉?如果把缓存的 feed 当作一个特性分支,把服务端的放回的 feed 当作 master 分支,那么我们要做的其实就是在 master 分支上执行 rebase 操作,把特性分支上的 commit(喜欢、评论等操作)应用到 master 分支中。

这样就有如下的设计:

  • 当页面加载,我们发送请求获取新数据(或等待服务端主动 push 新数据,参考【第2285期】让 Instagram.com 变得更快:提前刷新和渐进式HTML

  • 创建 1 个暂存的 Redux State 子集

  • 当请求/推送未完成时,把所有已经 dispatch 的 action 存储起来

  • 当请求/推送求完成,新的数据把暂存的State中的数据覆盖,此时把这些存储的 action 和其它等在在暂存的State中待执行的 action 都应用到最新的数据中

  • 当暂存的 State 提交的时候,我们直接把当前的 State 替换成暂存的 State

通过维持一个暂存状态的 State,所有已经存在的 reducer 行为都可以重复使用,并将暂存 State 和最新 State 分开。另外,我们是使用 Redux 来实现暂存的,因此可以直接使用 dispatch action,非常方便。

  1. function stagingAction(key: string, promise: Promise<Action>): AsyncAction<State, Action>;


  2. function stagingCommit(key: string): AsyncAction<State, Action>;

暂存 API 主要包含两个方法: stagingAction & stagingCommit (当然还有一些还原 State 的方法和处理边界情况的代码,这里就不展开了)

stagingAction 函数接收 1 个 Promise,这个 Promise 会 resolve 1 个要 dispatch 到暂存 State 的 action。它会初始化时暂存 State,然后保存所有已经 dispatch 的 actions。在 Git 概念里,我们可以把这个操作当作创建 1 个本地分支,因为在新数据到达前,发生的任何操作都将按顺序应用到暂存的State中。

stagingCommit 函数的作用是暂存的 State 提交到当前 State 中。如果暂存的 State 中还有其它的异步 actions 未执行完,它会一直等待。这非常像 Git 中 rebase 操作: 将所有的本地改动(从缓存的特性分支中来的改动)应用到 master 的 head 前面(服务端返回的新数据),让本地的 master 分支同时应用远程改动和本地改动,从而更新到最新。

为了更方便地使用暂存State,我们在根级的 reducer 上封装了一个 reducer 增强器,它处理所有要暂存的 action,并将所有已暂存的 actions 应用到新的 State 中。这样我们只需要 dispatch 相关 actions 就行,其它的事情这个增强器都处理好了。举个例子,如果我们要请求 1 个新的 feed,并应用到已暂存的 State 中,我们就执行类似下面的逻辑:

  1. function fetchAndStageFeed() {

  2. return stagingAction(

  3. 'feed',

  4. (async () => {

  5. const { data } = await fetchFeedTimeline();

  6. return {

  7. type: FEED_LOADED,

  8. ...data,

  9. };

  10. })()

  11. );

  12. }


  13. // 请求最新的 feed 并开始暂存

  14. store.dispatch(fetchAndStageFeed());


  15. // 在此期间,任何已经 dispatched 的actions都会被应用到暂存的State中的 feed

  16. // 直到 stagingCommit 提交暂存的State


  17. // 将暂存的State提交到当前的State中

  18. store.dispatch(stagingCommit('feed'));

我们对 Feed 和 Story 同时使用缓存优先渲染功能,使得页面渲染完成时间缩短了 2.5%和 11%,并且让页面更贴近原生 iOS 和 android的浏览体验。

译注

这边文章核心思路比较简单,比较有意思的地方是用Git版本控制的概念来处理冲突问题。其实软件工程中的需要思想都可以互相借鉴,比如前端MVC最早可能参考了Spring,微前端服务参考了后端的微服务。

关于本文 译者:@Smith 译文:https://zhuanlan.zhihu.com/p/100284673 作者:@Glenn Conner 原文:https://instagram-engineering.com/making-instagram-com-faster-part-3-cache-first-6f3f130b9669


为你推荐


【第1541期】资源优先级 – 让浏览器助您一臂之力


【第1926期】缓存控制中的 stale-while-revalidate


欢迎自荐投稿,前端早读课等你来。

继续滑动看下一个

【第2287期】让 Instagram.com 变得更快: 缓存优先

向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存