Skip to content

Commit

Permalink
🚀 [Story performance] Load first page assets before loading other pag…
Browse files Browse the repository at this point in the history
…es to improve LCP (#34846)

* Added lcp, tests, etc

* Cleaned up

* Added return

* Use webp

* Fixed imports

* Fixed page loading before distance set

* Fix isExperimentOn and overrideDistance

* Removed log

* Fixed setDistance on navigation

* Removed unused import / export

* Fix html checks

* Updated tests

* revert const to let

* Update extensions/amp-story/1.0/amp-story.js

Co-authored-by: Ryan Cebulko <ryan@cebulko.com>

* Simplified preloadAll call

* Removed comment

* Updated tests and removed getDistance

* Rely on store service

* Added csi experiment

* Add comment and move experiment

* Split tests in compressed / uncompressed

* Catch inside then

* Fixed test

Co-authored-by: Ryan Cebulko <ryan@cebulko.com>
  • Loading branch information
mszylkowski and rcebulko committed Jul 12, 2021
1 parent 38f8b58 commit 1594e79
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 4 deletions.
105 changes: 105 additions & 0 deletions examples/amp-story/performance/lcp-compressed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<!-- Use #load=first to enable the experiment. -->
<!doctype html>
<html âš¡ lang="en">
<head>
<meta charset="utf-8">
<link rel="canonical" href="lcp-compressed.html">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<title>LCP test</title>

<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&family=Source+Sans+Pro:wght@400;600&display=swap" rel="stylesheet">

<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<style amp-custom>
* {
box-sizing: border-box;
}
amp-story-page {
font-family: 'Source Sans Pro', sans-serif;
color: white;
background: black;
}
code {
font-family: 'Source Code Pro', monospace;
background: #fff2;
}
amp-story p {
line-height: 1.5;
}
.bold {
font-weight: bold;
}
.bottom {
align-content: end;
}
.last {
padding-bottom: 8.83vh;
}
.byline {
letter-spacing: 1.28px;
}
amp-img {
margin: 1em;
margin-bottom: 5em;
}
</style>
</head>
<body>
<script>
const loadFirst = location.hash.includes('load=first');
(self.AMP = self.AMP || []).push(function(AMP) {
AMP.toggleExperiment('amp-story-load-first-page-only', loadFirst);
});
</script>
<amp-story standalone id="cats"
title="LCP test" publisher="The AMP team"
publisher-logo-src="./img/amplogo.png"
poster-portrait-src="./img/overview.jpg">

<amp-story-page id="page-1" title="Lorem ipsum dolor sit amet">
<amp-story-grid-layer template="fill">
<amp-img
id="img1"
width="400"
height="750"
src="https://images.unsplash.com/photo-1515705576963-95cad62945b6?w=1750&q=30&fm=webp"
layout="fill">
</amp-img>
</amp-story-grid-layer>
<amp-story-grid-layer template="vertical" class="bottom">
<p class="bold">LCP for images (compressed)</p>
<p class="byline">Add <code>#load=first</code> to activate experiment</p>
</amp-story-grid-layer>
</amp-story-page>

<amp-story-page id="page-2">
<amp-story-grid-layer template="fill">
<amp-img
id="img2"
width="400"
height="750"
src="https://images.unsplash.com/photo-1502318217862-aa4e294ba657?w=1080&q=30&fm=webp"
layout="fill">
</amp-img>
</amp-story-grid-layer>
</amp-story-page>

<amp-story-page id="page-3">
<amp-story-grid-layer template="fill">
<amp-img
id="img3"
width="400"
height="750"
src="https://images.unsplash.com/photo-1513809491260-0e192158ae44?w=1080&q=30&fm=webp"
layout="fill">
</amp-img>
</amp-story-grid-layer>
</amp-story-page>
</amp-story>
</body>
</html>
105 changes: 105 additions & 0 deletions examples/amp-story/performance/lcp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<!-- Use #load=first to enable the experiment. -->
<!doctype html>
<html âš¡ lang="en">
<head>
<meta charset="utf-8">
<link rel="canonical" href="lcp.html">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<title>LCP test</title>

<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&family=Source+Sans+Pro:wght@400;600&display=swap" rel="stylesheet">

<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<style amp-custom>
* {
box-sizing: border-box;
}
amp-story-page {
font-family: 'Source Sans Pro', sans-serif;
color: white;
background: black;
}
code {
font-family: 'Source Code Pro', monospace;
background: #fff2;
}
amp-story p {
line-height: 1.5;
}
.bold {
font-weight: bold;
}
.bottom {
align-content: end;
}
.last {
padding-bottom: 8.83vh;
}
.byline {
letter-spacing: 1.28px;
}
amp-img {
margin: 1em;
margin-bottom: 5em;
}
</style>
</head>
<body>
<script>
const loadFirst = location.hash.includes('load=first');
(self.AMP = self.AMP || []).push(function(AMP) {
AMP.toggleExperiment('amp-story-load-first-page-only', loadFirst);
});
</script>
<amp-story standalone id="cats"
title="LCP test" publisher="The AMP team"
publisher-logo-src="./img/amplogo.png"
poster-portrait-src="./img/overview.jpg">

<amp-story-page id="page-1" title="Lorem ipsum dolor sit amet">
<amp-story-grid-layer template="fill">
<amp-img
id="img1"
width="400"
height="750"
src="https://images.unsplash.com/photo-1515705576963-95cad62945b6?w=1750&q=100&fm=webp"
layout="fill">
</amp-img>
</amp-story-grid-layer>
<amp-story-grid-layer template="vertical" class="bottom">
<p class="bold">LCP for images (uncompressed)</p>
<p class="byline">Add <code>#load=first</code> to activate experiment</p>
</amp-story-grid-layer>
</amp-story-page>

<amp-story-page id="page-2">
<amp-story-grid-layer template="fill">
<amp-img
id="img2"
width="400"
height="750"
src="https://images.unsplash.com/photo-1502318217862-aa4e294ba657?w=1080&q=100&fm=webp"
layout="fill">
</amp-img>
</amp-story-grid-layer>
</amp-story-page>

<amp-story-page id="page-3">
<amp-story-grid-layer template="fill">
<amp-img
id="img3"
width="400"
height="750"
src="https://images.unsplash.com/photo-1513809491260-0e192158ae44?w=1080&q=100&fm=webp"
layout="fill">
</amp-img>
</amp-story-grid-layer>
</amp-story-page>
</amp-story>
</body>
</html>
3 changes: 3 additions & 0 deletions extensions/amp-story/1.0/amp-story-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,9 @@ export class AmpStoryPage extends AMP.BaseElement {
if (this.isAd()) {
distance = Math.min(distance, 2);
}
if (distance == this.getDistance()) {
return;
}

this.element.setAttribute('distance', distance);
this.element.setAttribute('aria-hidden', distance != 0);
Expand Down
37 changes: 33 additions & 4 deletions extensions/amp-story/1.0/amp-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,11 @@ export class AmpStory extends AMP.BaseElement {
);
});
}
if (isExperimentOn(this.win, 'amp-story-load-first-page-only')) {
Services.performanceFor(this.win).addEnabledExperiment(
'amp-story-load-first-page-only'
);
}

if (this.maybeLoadStoryDevTools_()) {
return;
Expand Down Expand Up @@ -1528,7 +1533,7 @@ export class AmpStory extends AMP.BaseElement {
// the navigation happened, like preloading the following pages, or
// sending analytics events.
() => {
this.preloadPagesByDistance_();
this.preloadPagesByDistance_(/* prioritizeActivePage */ !oldPage);
this.triggerActiveEventForPage_();

this.systemLayer_.resetDeveloperLogs();
Expand Down Expand Up @@ -2216,8 +2221,11 @@ export class AmpStory extends AMP.BaseElement {
return map;
}

/** @private */
preloadPagesByDistance_() {
/**
* @param {boolean=} prioritizeActivePage
* @private
*/
preloadPagesByDistance_(prioritizeActivePage = false) {
if (this.platform_.isBot()) {
this.pages_.forEach((page) => {
page.setDistance(0);
Expand All @@ -2227,13 +2235,34 @@ export class AmpStory extends AMP.BaseElement {

const pagesByDistance = this.getPagesByDistance_();

this.mutateElement(() => {
const preloadAllPages = () => {
pagesByDistance.forEach((pageIds, distance) => {
pageIds.forEach((pageId) => {
const page = this.getPageById(pageId);
page.setDistance(distance);
});
});
};

this.mutateElement(() => {
if (
!isExperimentOn(this.win, 'amp-story-load-first-page-only') ||
!prioritizeActivePage
) {
return preloadAllPages();
}

const activePageId = devAssert(pagesByDistance[0][0]);
new Promise((res, rej) => {
const page = this.getPageById(activePageId);
page.setDistance(0);
page.signals().whenSignal(CommonSignals.LOAD_END).then(res);
// Don't call preload if user navigates before page loads, since the navigation will call preload properly.
this.storeService_.subscribe(StateProperty.CURRENT_PAGE_ID, rej);
}).then(
() => preloadAllPages(),
() => {}
);
});
}

Expand Down
61 changes: 61 additions & 0 deletions extensions/amp-story/1.0/test/test-amp-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import {Keys} from '#core/constants/key-codes';
import {LocalizationService} from '#service/localization';
import {MediaType} from '../media-pool';
import {PageState} from '../amp-story-page';
import {Performance} from '#service/performance-impl';
import {Services} from '#service';
import {Signals} from '#core/data-structures/signals';
import {VisibilityState} from '#core/constants/visibility-state';
import {createElementWithAttributes} from '#core/dom';
import {registerServiceBuilder} from '../../../../src/service-helpers';
Expand All @@ -58,6 +60,8 @@ describes.realWin(
let replaceStateStub;
let win;

const nextTick = () => new Promise((resolve) => win.setTimeout(resolve, 0));

/**
* @param {number} count
* @param {Array<string>=} ids
Expand Down Expand Up @@ -1837,5 +1841,62 @@ describes.realWin(
).to.be.false;
});
});

describe('experiment for amp-story-load-first-page-only', () => {
let pages;
let performanceImpl;
beforeEach(async () => {
toggleExperiment(win, 'amp-story-load-first-page-only', true);
performanceImpl = new Performance(env.win);
env.sandbox.stub(Services, 'performanceFor').returns(performanceImpl);
pages = await createStoryWithPages(2, ['page-1', 'page-2'], false);
env.sandbox.stub(story, 'mutateElement').callsFake((fn) => fn());
});

it('should position the active page so it preloads', async () => {
story.buildCallback();
await story.layoutCallback();

// Check page 0 is loaded with distance 0.
expect(pages[0].getAttribute('distance')).to.be.equal('0');
});

it('should not position the inactive page so it preloads before the active page is loaded', async () => {
const signals = new Signals();
pages[0].signals = () => signals;
story.buildCallback();
await story.layoutCallback();

// Check page 1 is not loaded.
expect(pages[1].hasAttribute('distance')).to.be.false;
});

it('should position the inactive page so it preloads after the active page is loaded', async () => {
const signals = new Signals();
pages[0].signals = () => signals;
story.buildCallback();
await story.layoutCallback();

// Check page 1 is not loaded.
expect(pages[1].hasAttribute('distance')).to.be.false;

signals.signal(CommonSignals.LOAD_END);
await nextTick();

// Check page 1 is loaded with distance 1.
expect(pages[1].getAttribute('distance')).to.be.equal('1');
});

it('should enable the CSI experiment', async () => {
const enableSpy = env.sandbox.spy(
performanceImpl,
'addEnabledExperiment'
);
story.buildCallback();
await story.layoutCallback();

expect(enableSpy).to.be.calledWith('amp-story-load-first-page-only');
});
});
}
);

0 comments on commit 1594e79

Please sign in to comment.