Skip to content

Commit

Permalink
[soft-navigations] Take paint area into account
Browse files Browse the repository at this point in the history
Currently, it's possible for a soft navigation to be triggered after a
non-visual element was appended to the DOM.
This CL modifies that, so that only if visual elements that comprise of
at least 20% of the initial (hard navigation) paint area were appended
to the DOM as a result of a user interaction, that would count as a soft
navigation.

Bug: 1479355
Change-Id: Id4609c6435c2ab3dde1a26fea14988e7cd171dad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4846835
Reviewed-by: Ian Clelland <iclelland@chromium.org>
Commit-Queue: Yoav Weiss <yoavweiss@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1207615}
  • Loading branch information
Yoav Weiss authored and SkyBlack1225 committed Dec 7, 2023
1 parent 579206c commit 1045599
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
const link = document.getElementById("link");

promise_test(async t => {
if (!performance.softNavPaintMetricsSupported) {
return;
}
validatePaintEntries('first-contentful-paint', 1);
validatePaintEntries('first-paint', 1);
const preClickLcp = await getLcpEntries();
Expand Down
137 changes: 80 additions & 57 deletions soft-navigation-heuristics/resources/soft-navigation-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ const testSoftNavigation =
const pushUrl = readValue(options.pushUrl, true);
const eventType = readValue(options.eventType, "click");
const interactionType = readValue(options.interactionType, 'click');
const expectLCP = options.validate != 'no-lcp';
const eventPrepWork = options.eventPrepWork;
promise_test(async t => {
await waitInitialLCP();
const preClickLcp = await getLcpEntries();
setEvent(t, link, pushState, addContent, pushUrl, eventType, eventPrepWork);
setEvent(t, link, pushState, addContent, pushUrl, eventType,
eventPrepWork);
let first_navigation_id;
for (let i = 0; i < clicks; ++i) {
const firstClick = (i === 0);
let paint_entries_promise =
waitOnPaintEntriesPromise(expectLCP && firstClick);
waitOnPaintEntriesPromise(firstClick);
interacted = false;
interact(link, interactionType);

await new Promise(resolve => {
(new PerformanceObserver(() => resolve())).observe({
type: 'soft-navigation'
});
});
const navigation_id = await waitOnSoftNav();
if (!first_navigation_id) {
first_navigation_id = navigation_id;
}
// Ensure paint timing entries are fired before moving on to the next
// click.
await paint_entries_promise;
Expand All @@ -49,7 +49,7 @@ const testSoftNavigation =
await validateSoftNavigationEntry(
clicks, extraValidations, pushUrl);

await runEntryValidations(preClickLcp, clicks + 1, expectLCP);
await runEntryValidations(preClickLcp, first_navigation_id, clicks + 1, options.validate);
}, testName);
};

Expand All @@ -64,17 +64,13 @@ const testNavigationApi = (testName, navigateEventHandler, link) => {
const preClickLcp = await getLcpEntries();
let paint_entries_promise = waitOnPaintEntriesPromise();
interact(link);
await new Promise(resolve => {
(new PerformanceObserver(() => resolve())).observe({
type: 'soft-navigation'
});
});
const first_navigation_id = await waitOnSoftNav();
await navigated;
await paint_entries_promise;
assert_equals(document.softNavigations, 1, 'Soft Navigation detected');
await validateSoftNavigationEntry(1, () => {}, 'foobar.html');

await runEntryValidations(preClickLcp);
await runEntryValidations(preClickLcp, first_navigation_id);
}, testName);
};

Expand Down Expand Up @@ -102,37 +98,34 @@ const testSoftNavigationNotDetected = options => {
};

const runEntryValidations =
async (preClickLcp, entries_expected_number = 2, expect_lcp = true) => {
await validatePaintEntries('first-contentful-paint', entries_expected_number);
await validatePaintEntries('first-paint', entries_expected_number);
async (preClickLcp, first_navigation_id, entries_expected_number = 2,
validate = null) => {
await validatePaintEntries('first-contentful-paint', entries_expected_number,
first_navigation_id);
await validatePaintEntries('first-paint', entries_expected_number,
first_navigation_id);
const postClickLcp = await getLcpEntries();
const postClickLcpWithoutSoftNavs = await getLcpEntriesWithoutSoftNavs();
if (expect_lcp) {
assert_greater_than(
postClickLcp.length, preClickLcp.length,
'Soft navigation should have triggered at least an LCP entry');
} else {
assert_equals(
postClickLcp.length, preClickLcp.length,
'Soft navigation should not have triggered an LCP entry');
assert_greater_than(
postClickLcp.length, preClickLcp.length,
'Soft navigation should have triggered at least an LCP entry');

if (validate) {
await validate();
}
assert_equals(
postClickLcpWithoutSoftNavs.length, preClickLcp.length,
'Soft navigation should not have triggered an LCP entry when the ' +
'observer did not opt in');
if (expect_lcp) {
assert_not_equals(
postClickLcp[postClickLcp.length - 1].size,
preClickLcp[preClickLcp.length - 1].size,
'Soft navigation LCP element should not have identical size to the hard ' +
'navigation LCP element');
} else {
assert_equals(
postClickLcp[postClickLcp.length - 1].size,
preClickLcp[preClickLcp.length - 1].size,
'Soft navigation LCP element should have an identical size to the hard ' +
'navigation LCP element');
}
assert_not_equals(
postClickLcp[postClickLcp.length - 1].size,
preClickLcp[preClickLcp.length - 1].size,
'Soft navigation LCP element should not have identical size to the hard ' +
'navigation LCP element');
assert_equals(
postClickLcp[preClickLcp.length].navigationId,
first_navigation_id, 'Soft navigation LCP should have the same navigation ' +
'ID as the last soft nav entry')
};

const interact =
Expand Down Expand Up @@ -214,7 +207,7 @@ const validateSoftNavigationEntry = async (clicks, extraValidations,

};

const validatePaintEntries = async (type, entries_number) => {
const validatePaintEntries = async (type, entries_number, first_navigation_id) => {
if (!performance.softNavPaintMetricsSupported) {
return;
}
Expand Down Expand Up @@ -242,6 +235,12 @@ const validatePaintEntries = async (type, entries_number) => {
assert_not_equals(entries[0].startTime, entries[1].startTime,
"Entries have different timestamps for " + type);
}
if (expected_entries_number > entries_without_softnavs.length) {
assert_equals(entries[entries_without_softnavs.length].navigationId,
first_navigation_id,
"First paint entry should have the same navigation ID as the last soft " +
"navigation entry");
}
};

const waitInitialLCP = () => {
Expand All @@ -253,6 +252,19 @@ const waitInitialLCP = () => {
});
}

const waitOnSoftNav = () => {
return new Promise(resolve => {
(new PerformanceObserver(list => {
const entries = list.getEntries();
assert_equals(entries.length, 1,
"Only one soft navigation entry");
resolve(entries[0].navigationId);
})).observe({
type: 'soft-navigation'
});
});
};

const getLcpEntries = async () => {
const entries = await new Promise(resolve => {
(new PerformanceObserver(list => resolve(
Expand All @@ -272,30 +284,41 @@ const getLcpEntriesWithoutSoftNavs = async () => {
return entries;
};

const addImage = async (element) => {
const addImage = async (element, url="blue.png") => {
const img = new Image();
img.src = '/images/blue.png' + "?" + Math.random();
img.src = '/images/'+ url + "?" + Math.random();
img.id="imagelcp";
await img.decode();
element.appendChild(img);
};
const addImageToMain = async () => {
await addImage(document.getElementById('main'));
const addImageToMain = async (url="blue.png") => {
await addImage(document.getElementById('main'), url);
};

const addTextToDivOnMain =
() => {
const main = document.getElementById("main");
const prevDiv = document.getElementsByTagName("div")[0];
if (prevDiv) {
main.removeChild(prevDiv);
}
const div = document.createElement("div");
const text = document.createTextNode("Lorem Ipsum");
div.appendChild(text);
div.style = "font-size: 3em";
main.appendChild(div);
}
const addTextParagraphToMain = (text, element_timing = "") => {
const main = document.getElementById("main");
const p = document.createElement("p");
const textNode = document.createTextNode(text);
p.appendChild(textNode);
if (element_timing) {
p.setAttribute("elementtiming", element_timing);
}
p.style = "font-size: 3em";
main.appendChild(p);
return p;
};
const addTextToDivOnMain = () => {
const main = document.getElementById("main");
const prevDiv = document.getElementsByTagName("div")[0];
if (prevDiv) {
main.removeChild(prevDiv);
}
const div = document.createElement("div");
const text = document.createTextNode("Lorem Ipsum");
div.appendChild(text);
div.style = "font-size: 3em";
main.appendChild(div);
}

const waitOnPaintEntriesPromise = (expectLCP = true) => {
return new Promise((resolve, reject) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/soft-navigation-heuristics/resources/soft-navigation-helper.js"></script>
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
</head>
<body>
<main id=main>
<div>
<a id=link><img id="initial_image1" src="/images/lcp-256x256.png?1" style="width: 90vw; height: 90vh">
<div id=extra></div>
</a>
</div>
</main>
<script>
(async () => {
const link = document.getElementById("link");

// Inject a second image that takes a large part of the viewport.
await new Promise(resolve => {
requestAnimationFrame(() => requestAnimationFrame(() => {
document.getElementById("extra").innerHTML = `
<div style="position: absolute; bottom: 0; right: 0">
<img id="initial_image2" src="/images/lcp-256x256.png?2" style="width: 90vw;height: 90vh">
</div>`;
resolve();
}));
});
// Wait until the second image is rendered.
await new Promise(resolve => {
requestAnimationFrame(() => requestAnimationFrame(() => {
resolve();
}));
});
testSoftNavigation({
addContent: async () => {
// Remove the initial image, so that the text would be painted on screen.
document.getElementById("initial_image1").remove();
document.getElementById("initial_image2").remove();
let lcp_element_painted;
const lcp_element_paint_promise = new Promise((r) => { lcp_element_painted = r; });
// Add an LCP element, but have it be small enough to not trigger the
// Soft Navigation heuristics.
const p = addTextParagraphToMain(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod",
/*element_timing=*/"lcp");
(new PerformanceObserver(list => {
// Once the first element is fully painted:
lcp_element_painted();
})).observe({type: "element", buffered: true});
await lcp_element_paint_promise;
// Add a smaller element that gets us over that threshold.
addTextParagraphToMain("dolore magna aliqua.");
},
link: link,
test: "Test that an image LCP followed by a smaller soft navigation LCP"
+ " properly queues an LCP entry, even when the soft navigation is"
+ " detected after the LCP, even when initial paints significantly"
+ " exceed the viewport dimensions."});
})();
</script>
</body>
</html>


42 changes: 42 additions & 0 deletions soft-navigation-heuristics/softnav-after-lcp-paint.tentative.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Detect simple soft navigation.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/soft-navigation-helper.js"></script>
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
</head>
<body>
<main id=main>
<div>
<a id=link><img src="/images/lcp-256x256.png"></a>
</div>
</main>
<script>
const link = document.getElementById("link");
testSoftNavigation({
addContent: async () => {
let lcp_element_painted;
const lcp_element_paint_promise = new Promise((r) => { lcp_element_painted = r; });
// Add an LCP element, but have it be small enough to not trigger the
// Soft Navigation heuristics.
const p = addTextParagraphToMain("Lorem Ipsu", /*element_timing=*/"lcp");
(new PerformanceObserver(list => {
// Once the first element is fully painted:
lcp_element_painted();
})).observe({type: "element", buffered: true});
await lcp_element_paint_promise;
// Add a smaller element that gets us over that threshold.
addTextParagraphToMain("m");
},
link: link,
test: "Test that an image LCP followed by a smaller soft navigation LCP"
+ " properly queues an LCP entry, even when the soft navigation is"
+ " detected after the LCP."});
</script>
</body>
</html>
52 changes: 52 additions & 0 deletions soft-navigation-heuristics/softnav-before-lcp-paint.tentative.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Detect simple soft navigation.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/soft-navigation-helper.js"></script>
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
</head>
<body>
<main id=main>
<div>
<a id=link><img src="/images/lcp-256x256.png"></a>
</div>
</main>
<script>
const link = document.getElementById("link");
testSoftNavigation({
addContent: async () => {
// Add an LCP element, large enough to trigger the Soft Navigation
// heuristics.
const p = addTextParagraphToMain("Lorem Ipsum", /*element_timing=*/"lcp");
p.id = "first_lcp";
// Once the first element is fully painted.
const observer = new PerformanceObserver(list => {
// Add a larger element to be the new LCP.
window.lcp_observer_promise = new Promise(resolve => {
(new PerformanceObserver(resolve)).observe({type: "element"});
});
const p2 = addTextParagraphToMain("LOREM IPSUMER", "real_lcp");
p2.id = "real_lcp";
observer.disconnect();
});
observer.observe({type: "element", buffered: true});
},
link: link,
validate: async () => {
await window.lcp_observer_promise;
const lcps = await getLcpEntries();
assert_greater_than_equal(lcps.length, 3, "Got at least 3 LCP entries");
assert_equals(lcps[lcps.length - 2].id, "first_lcp", "Got the first LCP");
assert_equals(lcps[lcps.length - 1].id, "real_lcp", "Got the real LCP");
},
test: "Test that an image LCP followed by 2 smaller soft navigation LCPs"
+ " properly queues both LCP entries, even when the soft navigation"
+ " is detected in between them."});
</script>
</body>
</html>
Loading

0 comments on commit 1045599

Please sign in to comment.