Skip to content

Commit e87aa03

Browse files
Yoav Weisschromium-wpt-export-bot
Yoav Weiss
authored andcommitted
[LCP] Add animated image support
This CL adds support for better handling of animated images in LCP: * A new attribute is exposing the first animated frame's paint time (behind a flag). * `startTime` is not changed. * The PageLoadMetrics reported for LCP are set to that first frame paint time for animated images (behind another flag). * Entries are not emitted until the image is loaded. Relevant spec issue: w3c/largest-contentful-paint#83 Change-Id: I6bb01eacb4f200f9c032ffcfcd9a1a41126a7773 Bug: 1260953
1 parent 1b3124b commit e87aa03

10 files changed

+204
-0
lines changed

images/anim-tao.png

460 Bytes
Loading

images/anim-tao.png.headers

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Timing-Allow-Origin: *
2+

images/webp-animated.webp

340 Bytes
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset=utf-8>
5+
<title>Largest Contentful Paint: observe image.</title>
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<script src="../resources/largest-contentful-paint-helpers.js"></script>
9+
</head>
10+
<body>
11+
<script>
12+
promise_test(async () => {
13+
assert_implements(window.LargestContentfulPaint,
14+
"LargestContentfulPaint is not implemented");
15+
const beforeLoad = performance.now();
16+
// 136 is the size of the animated GIF up until the first frame.
17+
// The trickle pipe delays the response after the first frame by 1 second.
18+
const url = window.location.origin +
19+
`/images/anim-gr.gif?pipe=trickle(136:d${delay_pipe_value})`;
20+
const entry = await load_and_observe(url);
21+
// anim-gr.gif is 100 by 50.
22+
const size = 100 * 50;
23+
checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
24+
}, "Same origin animated image is observable and has a first frame.");
25+
</script>
26+
</body>
27+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset=utf-8>
5+
<title>Largest Contentful Paint: observe image.</title>
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<script src="../resources/largest-contentful-paint-helpers.js"></script>
9+
</head>
10+
<body>
11+
<script>
12+
promise_test(async () => {
13+
assert_implements(window.LargestContentfulPaint,
14+
"LargestContentfulPaint is not implemented");
15+
const beforeLoad = performance.now();
16+
// 142 is the size of the animated WebP up until the first frame.
17+
// The trickle pipe delays the response after the first frame by 1 second.
18+
const url = window.location.origin +
19+
`/images/webp-animated.webp?pipe=trickle(142:d${delay_pipe_value})`;
20+
const entry = await load_and_observe(url);
21+
// webp-animated.webp is 11 by 29.
22+
const size = 11 * 29;
23+
checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
24+
}, "Same origin animated image is observable and has a first frame.");
25+
</script>
26+
</body>
27+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset=utf-8>
5+
<title>Largest Contentful Paint: observe image.</title>
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<script src="../resources/largest-contentful-paint-helpers.js"></script>
9+
</head>
10+
<body>
11+
<script>
12+
promise_test(async () => {
13+
assert_implements(window.LargestContentfulPaint,
14+
"LargestContentfulPaint is not implemented");
15+
const beforeLoad = performance.now();
16+
// 262 is the size of the animated PNG up until the first frame,
17+
// including the chunk that starts the second frame (indicating that
18+
// the first frame data is done).
19+
// The trickle pipe delays the response after the first frame by 1 second.
20+
const url = window.location.origin +
21+
`/images/anim-gr.png?pipe=trickle(262:d${delay_pipe_value})`;
22+
const entry = await load_and_observe(url);
23+
// anim-gr.png is 100 by 50.
24+
const size = 100 * 50;
25+
checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
26+
}, "Same origin animated image is observable and has a first frame.");
27+
</script>
28+
</body>
29+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset=utf-8>
5+
<title>Largest Contentful Paint: observe image.</title>
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<script src="../resources/largest-contentful-paint-helpers.js"></script>
9+
<script src="/common/get-host-info.sub.js"></script>
10+
</head>
11+
<body>
12+
<script>
13+
promise_test(async () => {
14+
assert_implements(window.LargestContentfulPaint,
15+
"LargestContentfulPaint is not implemented");
16+
const beforeLoad = performance.now();
17+
// 262 is the size of the animated PNG up until the first frame,
18+
// including the chunk that starts the second frame (indicating that
19+
//the first frame data is done).
20+
const {REMOTE_ORIGIN} = get_host_info();
21+
const url = REMOTE_ORIGIN +
22+
'/images/anim-gr.png?pipe=trickle(262:d1)';
23+
const entry = await load_and_observe(url);
24+
// anim-gr.png is 100 by 50.
25+
const size = 100 * 50;
26+
checkImage(entry, url, 'image_id', size, beforeLoad, ["renderTimeIs0", "animated-zero"]);
27+
}, "Same origin animated image is observable and has a first frame.");
28+
</script>
29+
</body>
30+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset=utf-8>
5+
<title>Largest Contentful Paint: observe image.</title>
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<script src="../resources/largest-contentful-paint-helpers.js"></script>
9+
<script src="/common/get-host-info.sub.js"></script>
10+
</head>
11+
<body>
12+
<script>
13+
promise_test(async () => {
14+
assert_implements(window.LargestContentfulPaint,
15+
"LargestContentfulPaint is not implemented");
16+
const beforeLoad = performance.now();
17+
// 262 is the size of the animated PNG up until the first frame,
18+
// including the chunk that starts the second frame (indicating that
19+
//the first frame data is done).
20+
const {REMOTE_ORIGIN} = get_host_info();
21+
const url = REMOTE_ORIGIN +
22+
'/images/anim-tao.png?pipe=trickle(262:d1)';
23+
const entry = await load_and_observe(url);
24+
// anim-gr.png is 100 by 50.
25+
const size = 100 * 50;
26+
checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
27+
}, "Same origin animated image is observable and has a first frame.");
28+
</script>
29+
</body>
30+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset=utf-8>
5+
<title>Largest Contentful Paint: observe image.</title>
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<script src="../resources/largest-contentful-paint-helpers.js"></script>
9+
</head>
10+
<body>
11+
<script>
12+
promise_test(async () => {
13+
assert_implements(window.LargestContentfulPaint,
14+
"LargestContentfulPaint is not implemented");
15+
const beforeLoad = performance.now();
16+
// 262 is the size of the animated PNG up until the first frame,
17+
// including the chunk that starts the second frame (indicating that
18+
//the first frame data is done).
19+
const url = window.location.origin + '/images/blue.png';
20+
const entry = await load_and_observe(url);
21+
// blue.png is 133 by 106.
22+
const size = 133 * 106;
23+
checkImage(entry, url, 'image_id', size, beforeLoad, ["animated-zero"]);
24+
}, "Same origin animated image is observable and has a first frame.");
25+
</script>
26+
</body>
27+
</html>

largest-contentful-paint/resources/largest-contentful-paint-helpers.js

+32
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
const image_delay = 1000;
2+
const delay_pipe_value = image_delay / 1000;
3+
14
// Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values.
25
// The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry.
36
// The |options| parameter may contain some string values specifying the following:
@@ -33,4 +36,33 @@ function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound
3336
} else {
3437
assert_equals(entry.size, expectedSize);
3538
}
39+
if (options.includes('animated')) {
40+
assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime,
41+
'firstAnimatedFrameTime should be smaller than loadTime');
42+
assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime,
43+
'firstAnimatedFrameTime should be smaller than renderTime');
44+
assert_less_than(entry.firstAnimatedFrameTime, image_delay,
45+
'firstAnimatedFrameTime should be smaller than the delay applied to the second frame');
46+
assert_greater_than(entry.firstAnimatedFrameTime, 0,
47+
'firstAnimatedFrameTime should be larger than 0');
48+
}
49+
if (options.includes('animated-zero')) {
50+
assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0');
51+
}
3652
}
53+
54+
const load_and_observe = url => {
55+
return new Promise(resolve => {
56+
(new PerformanceObserver(entryList => {
57+
for (let entry of entryList.getEntries()) {
58+
if (entry.url == url) {
59+
resolve(entryList.getEntries()[0]);
60+
}
61+
}
62+
})).observe({type: 'largest-contentful-paint', buffered: true});
63+
const img = new Image();
64+
img.id = 'image_id';
65+
img.src = url;
66+
document.body.appendChild(img);
67+
});
68+
};

0 commit comments

Comments
 (0)