[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:
https://github.com/WICG/largest-contentful-paint/issues/83
Change-Id: I6bb01eacb4f200f9c032ffcfcd9a1a41126a7773
Bug: 1260953
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3226157
Commit-Queue: Yoav Weiss <yoavweiss@chromium.org>
Reviewed-by: Nicolás Peña Moreno <npm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#935133}
diff --git a/images/anim-tao.png b/images/anim-tao.png
new file mode 100644
index 0000000..925e2ef
--- /dev/null
+++ b/images/anim-tao.png
Binary files differ
diff --git a/images/anim-tao.png.headers b/images/anim-tao.png.headers
new file mode 100644
index 0000000..0230e17
--- /dev/null
+++ b/images/anim-tao.png.headers
@@ -0,0 +1,2 @@
+Timing-Allow-Origin: *
+
diff --git a/images/webp-animated.webp b/images/webp-animated.webp
new file mode 100644
index 0000000..35a8dfc
--- /dev/null
+++ b/images/webp-animated.webp
Binary files differ
diff --git a/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html b/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
new file mode 100644
index 0000000..a2c0d79
--- /dev/null
+++ b/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 136 is the size of the animated GIF up until the first frame.
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/images/anim-gr.gif?pipe=trickle(136:d${delay_pipe_value})`;
+ const entry = await load_and_observe(url);
+ // anim-gr.gif is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html b/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
new file mode 100644
index 0000000..de59d5c
--- /dev/null
+++ b/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 142 is the size of the animated WebP up until the first frame.
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/images/webp-animated.webp?pipe=trickle(142:d${delay_pipe_value})`;
+ const entry = await load_and_observe(url);
+ // webp-animated.webp is 11 by 29.
+ const size = 11 * 29;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/largest-contentful-paint/animated/observe-animated-image.tentative.html b/largest-contentful-paint/animated/observe-animated-image.tentative.html
new file mode 100644
index 0000000..cf7d262
--- /dev/null
+++ b/largest-contentful-paint/animated/observe-animated-image.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ // the first frame data is done).
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/images/anim-gr.png?pipe=trickle(262:d${delay_pipe_value})`;
+ const entry = await load_and_observe(url);
+ // anim-gr.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html b/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
new file mode 100644
index 0000000..993883c
--- /dev/null
+++ b/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ //the first frame data is done).
+ const {REMOTE_ORIGIN} = get_host_info();
+ const url = REMOTE_ORIGIN +
+ '/images/anim-gr.png?pipe=trickle(262:d1)';
+ const entry = await load_and_observe(url);
+ // anim-gr.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["renderTimeIs0", "animated-zero"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html b/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
new file mode 100644
index 0000000..137dde6
--- /dev/null
+++ b/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ //the first frame data is done).
+ const {REMOTE_ORIGIN} = get_host_info();
+ const url = REMOTE_ORIGIN +
+ '/images/anim-tao.png?pipe=trickle(262:d1)';
+ const entry = await load_and_observe(url);
+ // anim-gr.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/largest-contentful-paint/animated/observe-non-animated-image.tentative.html b/largest-contentful-paint/animated/observe-non-animated-image.tentative.html
new file mode 100644
index 0000000..6bbc095
--- /dev/null
+++ b/largest-contentful-paint/animated/observe-non-animated-image.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ //the first frame data is done).
+ const url = window.location.origin + '/images/blue.png';
+ const entry = await load_and_observe(url);
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated-zero"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/largest-contentful-paint/resources/largest-contentful-paint-helpers.js b/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
index e12ece0..5012faf 100644
--- a/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
+++ b/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
@@ -1,3 +1,6 @@
+const image_delay = 1000;
+const delay_pipe_value = image_delay / 1000;
+
// Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values.
// The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry.
// The |options| parameter may contain some string values specifying the following:
@@ -33,4 +36,33 @@
} else {
assert_equals(entry.size, expectedSize);
}
+ if (options.includes('animated')) {
+ assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime,
+ 'firstAnimatedFrameTime should be smaller than loadTime');
+ assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime,
+ 'firstAnimatedFrameTime should be smaller than renderTime');
+ assert_less_than(entry.firstAnimatedFrameTime, image_delay,
+ 'firstAnimatedFrameTime should be smaller than the delay applied to the second frame');
+ assert_greater_than(entry.firstAnimatedFrameTime, 0,
+ 'firstAnimatedFrameTime should be larger than 0');
+ }
+ if (options.includes('animated-zero')) {
+ assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0');
+ }
}
+
+const load_and_observe = url => {
+ return new Promise(resolve => {
+ (new PerformanceObserver(entryList => {
+ for (let entry of entryList.getEntries()) {
+ if (entry.url == url) {
+ resolve(entryList.getEntries()[0]);
+ }
+ }
+ })).observe({type: 'largest-contentful-paint', buffered: true});
+ const img = new Image();
+ img.id = 'image_id';
+ img.src = url;
+ document.body.appendChild(img);
+ });
+};