从 chromium 源码分析缓存新鲜度

578 阅读3分钟

前言

HTTP从发明至今,已有30年有余。作为一个前端工程师而言,了解其规范甚至实现细节可以更好的帮助设计高性能的网站以及更快速的 debug。本文从 chromium 对 HTTP 协议缓存新鲜度的计算实现,来窥探一下协议实现者视角下的设计思路。

代码见链接:

source.chromium.org/chromium/ch…

分析

// From RFC 2616 section 13.2.4:
//
// The calculation to determine if a response has expired is quite simple:
//
//   response_is_fresh = (freshness_lifetime > current_age)
//
// Of course, there are other factors that can force a response to always be
// validated or re-fetched.
//
// From RFC 5861 section 3, a stale response may be used while revalidation is
// performed in the background if
//
//   freshness_lifetime + stale_while_revalidate > current_age

这段注释描述了最核心的算法,是否新鲜取决于 freshness_lifetime 和 current_age 两个值的大小。对于另一种特殊情况我们下文再聊。那问题就来到了 freshness_lifetime 和 current_age 这两个变量的定义。先搜索 freshness_lifetime,找找有没有关于他的注释。

很容易就在下文找到关于 freshness_lifetime 的定义,我们一段一段的分析。

// From RFC 2616 section 13.2.4:
//
// The max-age directive takes priority over Expires, so if max-age is present
// in a response, the calculation is simply:
//
//   freshness_lifetime = max_age_value

如果响应头中有 max-age 这个 header,那么 freshness_lifetime 就等于 max-age 的值

// Otherwise, if Expires is present in the response, the calculation is:
//
//   freshness_lifetime = expires_value - date_value

如果没有 max-age 有 expires,那么 freshness_lifetime 的值就是 expires 减 date 的值。

如果响应头里没有 date,那么就把收到请求的时间当做 date,可以搜索 date 的定义,如下:

// If there is no Date header, then assume that the server response was
// generated at the time when we received the response.
// Also, if the response does have a Last-Modified time, the heuristic
// expiration value SHOULD be no more than some fraction of the interval since
// that time. A typical setting of this fraction might be 10%:
//
//   freshness_lifetime = (date_value - last_modified_value) * 0.10

如果以上都没有,并且有 last-modified 这个 header,那么 freshness_lifetime 的值为 date 和 last_modified 差的 0.1 倍。当然这种情况是在 cache-control 没有被广泛使用时的一种解决方法,从这个 0.1 的常量也能看出来是一种实践中的妥协。

注释看完之后其实大约知道计算的逻辑了,继续看一下 GetFreshnessLifetimes 的实现看看有没有一些值得关注的细节。

if (HasHeaderValue("cache-control", "no-cache") ||
      HasHeaderValue("cache-control", "no-store") ||
      HasHeaderValue("pragma", "no-cache")) {
    return lifetimes;
  }

如果请响应头中的 cache-control 为 no-cache 或 no-store,pragma 为 no-cache,那么直接返回 lifetimes 的初始值也就是 0。

  if (GetMaxAgeValue(&lifetimes.freshness))
    return lifetimes;

如果有 max-age,那么就把 max-age 的值赋值给 lifetimes(&是c++中的取地址运算符,可以简单理解成赋值),上文分析的第一种情况。

  if (!GetDateValue(&date_value))
    date_value = response_time;

如果没有 date 这个 header,那么就用响应时间,上文已经提到过了。

    if (expires_value > date_value) {
      lifetimes.freshness = expires_value - date_value;
      return lifetimes;
    }

如果 expires_value > date_value,那么 lifetimes 就是 expires_value - date_value,上文分析的注释的第二种情况。

  if ((response_code_ == net::HTTP_OK ||
       response_code_ == net::HTTP_NON_AUTHORITATIVE_INFORMATION ||
       response_code_ == net::HTTP_PARTIAL_CONTENT) &&
      !must_revalidate) {
    // TODO(darin): Implement a smarter heuristic.
    Time last_modified_value;
    if (GetLastModifiedValue(&last_modified_value)) {
      // The last-modified value can be a date in the future!
      if (last_modified_value <= date_value) {
        lifetimes.freshness = (date_value - last_modified_value) / 10;
        return lifetimes;
      }
    }
  }

如果 HTTP 的状态码是 200、203、206 ,cache-control 不是 must-revalidate,并且有 last-modified 有值,那么 lifetimes 的值就为 (date_value - last_modified_value) / 10,也就是上文注释中的第三种情况。

// These responses are implicitly fresh (unless otherwise overruled):
if (response_code_ == net::HTTP_MULTIPLE_CHOICES ||
      response_code_ == net::HTTP_MOVED_PERMANENTLY ||
      response_code_ == net::HTTP_PERMANENT_REDIRECT ||
      response_code_ == net::HTTP_GONE) {
    lifetimes.freshness = base::TimeDelta::Max();
    lifetimes.staleness = base::TimeDelta();  // It should never be stale.
    return lifetimes;
  }

然后是隐性的新鲜度,如果 HTTP 的状态码是 300、301、308、410(也就是永久的404),那么 lifetimes 的值无限大。

最后,如果什么情况都没命中,就return lifetimes也就是 0。

开篇提到的一段注释 freshness_lifetime + stale_while_revalidate > current_age可以这么理解:请求已经过期了,但是希望浏览器给这个缓存一个“死缓”,先用当前的缓存,然后再发一个请求去验证缓存是否真的过期,如果过期了就用新的,没过期就更新新鲜度。web.dev/i18n/zh/sta…

小结

  1. 如果 cache-control:no-cache,no-store 或者 pragma:no-cache 存在,那么 freshness_lifetime 为 0
  2. 如果存在 cache-control: max-age,那么 freshness_lifetime 为 max-age 的值
  3. 如果存在 expires,那么 freshness_lifetime 为 expires - date。date 没有就是相应收到的时间。
  4. 如果有 last-modified,并且没有指定 must-revalidate,那么 freshness_lifetime 为 (date - last-modified) * 0.1

总结

关于浏览器的缓存策略相关的知识点非常多,本文并没有在描述一个全貌,而是从一个很小的点去挖掘。希望能提供一个学习在知识框架搭建完成之后填补细节的方法,感谢大家阅读。