原地址 https://lulus.life/2018/08/24/lazyimage/
Blink LazyImages 介绍
首先通过视频看一下效果
视频地址
视频解释
这个功能大概就是,当图片快滑到视图之内的时候,再去请求加载图片
主要需要注意以下几点
- 页面刚加载的时候图片在viewport之外, 这时图片没有全部加载
- 图片资源只发出了一个206请求,用来获取前 2048字节
- 当滑动页面,致使图片靠近viewport的时候,浏览器发出请求,请求整个图片资源。
如何启用该功能
需要注意的事项
网站里的图片最好有 etag 或者 Date + Last Modified
原因:
chrmium 只会对于有etag 或者 Date - Last Modified > 60sec的资源建立sparse缓存, 这样的话,当图片离viewport比较远的时候,首先blink会发出一个带有range的请求,请求资源的0-2047个字节。 当滑动过程中图片即将进入viewport的时候,会再次发出一个请求请求整个图片。 如果没有sparse缓存的话, 第二个请求会请求一个完完整整的图片数据,当我们有了sparse缓存,第二个请求首先会先从sparse缓存里读取2048个字节,然后再从网络请求剩下的内容
是否创建sparse文件由以下函数决定
bool HttpUtil::HasStrongValidators(HttpVersion version,
const std::string& etag_header,
const std::string& last_modified_header,
const std::string& date_header) {
if (!HasValidators(version, etag_header, last_modified_header))
return false;
if (version < HttpVersion(1, 1))
return false;
if (!etag_header.empty()) {
size_t slash = etag_header.find('/');
if (slash == std::string::npos || slash == 0)
return true;
std::string::const_iterator i = etag_header.begin();
std::string::const_iterator j = etag_header.begin() + slash;
TrimLWS(&i, &j);
if (!base::LowerCaseEqualsASCII(base::StringPiece(i, j), "w"))
return true;
}
base::Time last_modified;
if (!base::Time::FromString(last_modified_header.c_str(), &last_modified))
return false;
base::Time date;
if (!base::Time::FromString(date_header.c_str(), &date))
return false;
// Last-Modified is implicitly weak unless it is at least 60 seconds before
// the Date value.
return ((date - last_modified).InSeconds() >= 60);
}
另外介绍一下 spase文件, 对于 有range的请求 chromium会把资源放到一个 {url的hash, 8个字节}_s文件里
举个例子:
首先我们清楚chrome的缓存
wanghui@wh:/tmp/chrome/Default/Cache$ ls
wanghui@wh:/tmp/chrome/Default/Cache$
可以看到我的缓存是空的。
然后创建以下页面
<html>
<head>
<style type="text/css">
#test {
height:2000px;
}
</style>
</head>
<div id="test">
lazy load image
</div>
<img src="https://lulus.life/01.jpg" />
</html>
用我们的chrome访问该页面
wanghui@wh:~/chromium/src$ out/Default/chrome --user-data-dir=/tmp/chrome --no-proxy-server --enable-quic --origin-to-force-quic-on=lulus.life:443 --host-resolver-rules='MAP lulus.life:443 127.0.0.1:6121' https://lulus.life/test.html
我用的quic服务器,忽略我的命令行
看一下我的缓存目录
wanghui@wh:/tmp/chrome/Default/Cache$ ls -l
total 28
-rw------- 1 wanghui wanghui 4049 Aug 24 18:39 235add38b243d26b_0
-rw------- 1 wanghui wanghui 2129 Aug 24 18:39 235add38b243d26b_s
-rw------- 1 wanghui wanghui 4213 Aug 24 18:39 8f505f077bd94c0e_0
-rw------- 1 wanghui wanghui 4001 Aug 24 18:39 bf9ed9cbda59708c_0
-rw------- 1 wanghui wanghui 24 Aug 24 18:39 index
drwx------ 2 wanghui wanghui 4096 Aug 24 18:39 index-dir
wanghui@wh:/tmp/chrome/Default/Cache$
打开 235add38b243d26b_s文件
1 235add38b243d26b_s + X
00000000: 305c 72a7 1b6d fbfc 0800 0000 1900 0000 0\r..m..........
00000010: 58ca eab6 0000 0000 6874 7470 733a 2f2f X.......https://
00000020: 6c75 6c75 732e 6c69 6665 2f30 312e 6a70 lulus.life/01.jp
00000030: 676b 6753 6501 bf97 eb00 0000 0000 0000 gkgSe...........
00000040: 0000 0800 0000 0000 005a 4219 8d00 0000 .........ZB.....
00000050: 00ff d8ff e000 104a 4649 4600 0101 0100 .......JFIF.....
00000060: 6000 6000 00ff db00 4300 0201 0102 0101 `.`.....C.......
00000070: 0202 0202 0202 0202 0305 0303 0303 0306 ................
00000080: 0404 0305 0706 0707 0706 0707 0809 0b09 ................
00000090: 0808 0a08 0707 0a0d 0a0a 0b0c 0c0c 0c07 ................
000000a0: 090e 0f0d 0c0e 0b0c 0c0c ffdb 0043 0102 .............C..
000000b0: 0202 0303 0306 0303 060c 0807 080c 0c0c ................
000000c0: 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c ................
000000d0: 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c ................
000000e0: 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c 0c0c 0cff ................
000000f0: c000 1108 0154 00ff 0301 2200 0211 0103 .....T....".....
00000100: 1101 ffc4 001f 0000 0105 0101 0101 0101 ................
00000110: 0000 0000 0000 0000 0102 0304 0506 0708 ................
00000120: 090a 0bff c400 b510 0002 0103 0302 0403 ................
00000130: 0505 0404 0000 017d 0102 0300 0411 0512 .......}........
00000140: 2131 4106 1351 6107 2271 1432 8191 a108 !1A..Qa."q.2....
00000150: 2342 b1c1 1552 d1f0 2433 6272 8209 0a16 #B...R..$3br....
00000160: 1718 191a 2526 2728 292a 3435 3637 3839 ....%&'()*456789
00000170: 3a43 4445 4647 4849 4a53 5455 5657 5859 :CDEFGHIJSTUVWXY
00000180: 5a63 6465 6667 6869 6a73 7475 7677 7879 Zcdefghijstuvwxy
00000190: 7a83 8485 8687 8889 8a92 9394 9596 9798 z...............
000001a0: 999a a2a3 a4a5 a6a7 a8a9 aab2 b3b4 b5b6 ................
000001b0: b7b8 b9ba c2c3 c4c5 c6c7 c8c9 cad2 d3d4 ................
000001c0: d5d6 d7d8 d9da e1e2 e3e4 e5e6 e7e8 e9ea ................
000001d0: f1f2 f3f4 f5f6 f7f8 f9fa ffc4 001f 0100 ................
000001e0: 0301 0101 0101 0101 0101 0000 0000 0000 ................
000001f0: 0102 0304 0506 0708 090a 0bff c400 b511 ................
00000200: 0002 0102 0404 0304 0705 0404 0001 0277 ...............w
通过上一篇 介绍http cache的文章我们可以知道,这个数据属于
https://lulus.life/01.jpg
只不过是一部分数据
接下来我们滑动窗口,让图片进入viewport
再次看一下cache目录
wanghui@wh:/tmp/chrome/Default/Cache$ ls -l
total 56
-rw------- 1 wanghui wanghui 4049 Aug 24 18:43 235add38b243d26b_0
-rw------- 1 wanghui wanghui 29164 Aug 24 18:43 235add38b243d26b_s
-rw------- 1 wanghui wanghui 4213 Aug 24 18:43 8f505f077bd94c0e_0
-rw------- 1 wanghui wanghui 4001 Aug 24 18:43 bf9ed9cbda59708c_0
-rw------- 1 wanghui wanghui 24 Aug 24 18:43 index
drwx------ 2 wanghui wanghui 4096 Aug 24 18:43 index-dir
wanghui@wh:/tmp/chrome/Default/Cache$
可以看到235add38b243d26b_s 文件大小涨到了29164,也就是图片的大小。
LazyImage 相关代码
LazyImage相关提交关联这个Issule 846170 (需要翻墙)
如果开启该功能后,所有图片会预先加载前0-2047字节, 并创建一个图片作为placeholder。
看一个堆栈
void ImageLoader::DoUpdateFromElement(BypassMainWorldBehavior bypass_behavior,
UpdateFromElementBehavior update_behavior,
const KURL& url,
ReferrerPolicy referrer_policy,
UpdateType update_type) {
...
FetchParameters params(resource_request, resource_loader_options);
ConfigureRequest(params, bypass_behavior, *element_,
document.GetFrame()->GetClientHintsPreferences());
if (update_behavior != kUpdateForcedReload &&
lazy_image_load_state_ == LazyImageLoadState::kNone) {
const auto* frame = document.GetFrame();
if (frame->IsClientLoFiAllowed(params.GetResourceRequest())) {
params.SetClientLoFiPlaceholder();
} else if (auto* html_image = ToHTMLImageElementOrNull(GetElement())) {
if (html_image->ElementCreatedByParser() &&
frame->IsLazyLoadingImageAllowed()) {
params.SetAllowImagePlaceholder();
lazy_image_load_state_ = LazyImageLoadState::kDeferred;
}
}
}
当允许lazyload的时候设置params为 AllowImagePlaceholder
bool LocalFrame::IsLazyLoadingImageAllowed() const {
if (!RuntimeEnabledFeatures::LazyImageLoadingEnabled())
return false;
if (Owner() && !Owner()->ShouldLazyLoadChildren())
return false;
return true;
}
所以Image会先以imageholder的方式呈现。imageholder的加载流程下次分析, imageholder是根据图片前0-2047字节创建的一个图片, 前2048字节一般包含图片的header,从此可以解析出图片的宽高以及tumbnail等信息
lazyimage主要的类是LazyLoadImageObserver,由这个类来控制是否需要加载整个图片。
继续看看堆栈
DoUpdateFromElement接下来会调用UpdateImageState, 而UpdateImageState会调用LazyLoadImageObserver的静态函数
StartMonitoring, 我们看一下这个函数
void LazyLoadImageObserver::StartMonitoring(Element* element) {
if (LocalFrame* frame = element->GetDocument().GetFrame()) {
if (Document* document = frame->LocalFrameRoot().GetDocument()) {
document->EnsureLazyLoadImageObserver()
.lazy_load_intersection_observer_->observe(element);
}
}
}
可以看到该函数首先会调用document的EnsureLazyLoadImageObserver
LazyLoadImageObserver& Document::EnsureLazyLoadImageObserver() {
if (!lazy_load_image_observer_)
lazy_load_image_observer_ = new LazyLoadImageObserver(*this);
return *lazy_load_image_observer_;
}
EnsureLazyLoadImageOberser判断如果当前document没有创建LazyLoadImageObserver就会创建一个LazyLoadImageObserver赋值给成员变量 lazy_load_image_observer_.
接下来看一下LazyLoadImageObserver的构造函数
这个LazyLoadImageObserver会创建一个IntersectionObserver 代码在 third_party/blink/renderer/core/html/lazy_load_image_observer.cc
LazyLoadImageObserver::LazyLoadImageObserver(Document& document) {
DCHECK(RuntimeEnabledFeatures::LazyImageLoadingEnabled());
document.AddConsoleMessage(ConsoleMessage::Create(
kInterventionMessageSource, kInfoMessageLevel,
"Images loaded lazily and replaced with placeholders. Load events are "
"deferred. See https://crbug.com/846170"));
lazy_load_intersection_observer_ = IntersectionObserver::Create(
{Length(kLazyLoadRootMarginPx, kFixed)},
{std::numeric_limits<float>::min()}, &document,
WTF::BindRepeating(&LazyLoadImageObserver::LoadIfNearViewport,
WrapWeakPersistent(this)));
}
其中 kLazyLoadRootMarginPx = 800
我们先看一下IntersectionObserver的构造函数
IntersectionObserver::IntersectionObserver(
IntersectionObserverDelegate& delegate,
Element* root,
const Vector<Length>& root_margin,
const Vector<float>& thresholds,
bool track_visibility)
: ContextClient(delegate.GetExecutionContext()),
delegate_(&delegate),
root_(root),
thresholds_(thresholds),
top_margin_(kFixed),
right_margin_(kFixed),
bottom_margin_(kFixed),
left_margin_(kFixed),
last_run_time_(-s_v2_throttle_delay_),
root_is_implicit_(root ? 0 : 1),
track_visibility_(track_visibility ? 1 : 0) {
...
if (Document* document = TrackingDocument())
document->EnsureIntersectionObserverController().AddTrackedObserver(*this);
}
可以看到这个observer加到了document的observercontroller里
再看一个堆栈
当页面viewport有更新的时候会计算observe的元素,是否符合要求,如果符合会返回给observer所有符合条件的element。
接下来就会调用 LazyLoadImageObserver::LoadIfNearViewport
void LazyLoadImageObserver::LoadIfNearViewport(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
for (auto entry : entries) {
if (!entry->isIntersecting())
continue;
Element* element = entry->target();
if (auto* image_element = ToHTMLImageElementOrNull(element))
image_element->LoadDeferredImage();
// Load the background image if the element has one deferred.
if (const ComputedStyle* style = element->GetComputedStyle())
style->LoadDeferredImages(element->GetDocument());
lazy_load_intersection_observer_->unobserve(element);
}
}
然后就是请求整个图片的数据。