Skip to content

Commit

Permalink
perf(live): improve performance by throttling renders
Browse files Browse the repository at this point in the history
  • Loading branch information
bglw committed Feb 24, 2022
1 parent 939be4c commit c34e2d4
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 6 deletions.
4 changes: 2 additions & 2 deletions javascript-modules/generate/lib/live-connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const getLiveEditingConnector = () => {
preferBlobs: true
});
const options = window.bookshopLiveOptions || {};
await window.bookshopLive.update(frontMatter, options);
CloudCannon?.refreshInterface?.();
const rendered = await window.bookshopLive.update(frontMatter, options);
if (rendered) CloudCannon?.refreshInterface?.();
}
document.addEventListener('cloudcannon:update', updateBookshopLive);
updateBookshopLive();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Feature: Eleventy Bookshop CloudCannon Live Editing
} ]
}
"""
* 🌐 "window.bookshopLive?.hasRendered === true" evaluates
* 🌐 "window.bookshopLive?.renderCount > 0" evaluates
Then 🌐 The selector h1 should contain "Gidday"
# Testing CloudCannon data changing
When 🌐 CloudCannon pushes new json:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
Feature: Bookshop CloudCannon Live Editing Performance

Background:
Given the file tree:
"""
package.json from starters/generate/package.json # <-- this .json line hurts my syntax highlighting
component-lib/
go.mod from starters/hugo/components.go.mod
config.toml from starters/hugo/components.config.toml
bookshop/
bookshop.config.js from starters/hugo/bookshop.config.js
site/
go.mod from starters/hugo/site.go.mod
config.toml from starters/hugo/site.config.toml
"""
* a component-lib/components/single/single.hugo.html file containing:
"""
<h1>{{ .title }}</h1>
"""
* a site/layouts/index.html file containing:
"""
<html>
<body>
{{ partial "bookshop_bindings" `(dict "title" .Params.block.title)` }}
{{ partial "bookshop" (slice "single" (dict "title" .Params.block.title)) }}
</body>
</html>
"""
* [front_matter]:
"""
block:
title: "Hello There"
"""
* a site/content/_index.md file containing:
"""
---
[front_matter]
---
"""
* [ssg]: "hugo"

@web
Scenario: Bookshop live renders on a throttle
Given 🌐 I have loaded my site in CloudCannon
When 🌐 CloudCannon pushes new yaml:
"""
block:
title: "Rerendered"
"""
* 🌐 CloudCannon pushes new yaml:
"""
block:
title: "Rerendered 2"
"""
* 🌐 CloudCannon pushes new yaml:
"""
block:
title: "Rerendered 3"
"""
* 🌐 CloudCannon pushes new yaml:
"""
block:
title: "Rerendered 4"
"""
* 🌐 CloudCannon pushes new yaml:
"""
block:
title: "Rerendered 5"
"""
Then 🌐 There should be no errors
* 🌐 There should be no logs
* 🌐 "window.bookshopLive?.renderCount === 2" should evaluate
* 🌐 The selector h1 should contain "Rerendered 5"
# Double check that it didn't lag another render
* 🌐 "window.bookshopLive?.renderCount === 2" should evaluate
11 changes: 10 additions & 1 deletion javascript-modules/integration-tests/support/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ Then(/^🌐 The selector (\S+) should match "(.+)"$/i, { timeout: 60 * 1000 }, a
assert.equal(outerHTML, contains ? outerHTML : `outerHTML containing \`${contents}\``);
});

Then(/^🌐 "(.+)" should evaluate$/i, { timeout: 5 * 1000 }, async function (statement) {
if (!this.page) throw Error("No page open");
try {
await this.page.waitForFunction(statement, { timeout: 4 * 1000 });
} catch (e) {
throw Error(`${statement} didn't evaluate within 4s`)
}
});

Then(/^🌐 There should be no errors$/i, { timeout: 60 * 1000 }, async function () {
assert.deepEqual(this.puppeteerErrors(), []);
});
Expand Down Expand Up @@ -346,7 +355,7 @@ Given(/^🌐 I (?:have loaded|load) my site( in CloudCannon)?$/i, { timeout: 60
// Trigger cloudcannon:load
await readyCloudCannon(page_data, this);
try {
await this.page.waitForFunction("window.bookshopLive?.hasRendered === true", { timeout: 4 * 1000 });
await this.page.waitForFunction("window.bookshopLive?.renderCount > 0", { timeout: 4 * 1000 });
} catch (e) {
this.trackPuppeteerError(e.toString());
this.trackPuppeteerError(`Bookshop didn't do an initial render within 4s`);
Expand Down
21 changes: 19 additions & 2 deletions javascript-modules/live/lib/app/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export const getLive = (engines) => class BookshopLive {
this.globalData = {};
this.data = {};
this.renderOptions = {};
this.hasRendered = false;
this.renderCount = 0;
this.renderedAt = 0;
this.shouldRenderAt = null;
this.renderFrequency = 1000;
this.renderTimeout = null;
this.awaitingDataFetches = options?.remoteGlobals?.length || 0;
options?.remoteGlobals?.forEach(this.fetchGlobalData.bind(this));
}
Expand Down Expand Up @@ -61,6 +65,7 @@ export const getLive = (engines) => class BookshopLive {
}

async update(data, options) {
const now = Date.now();
// transformData = false means implementations like Jekyll
// won't wrap the data in { page: {} }
// (this is currently only used for tests)
Expand All @@ -74,8 +79,19 @@ export const getLive = (engines) => class BookshopLive {
while (this.awaitingDataFetches > 0) {
await sleep(100);
}
if (now - this.renderedAt < this.renderFrequency) {
const shouldRenderAt = this.renderedAt + this.renderFrequency;
this.shouldRenderAt = shouldRenderAt;
await sleep(shouldRenderAt - now);
if (shouldRenderAt !== this.shouldRenderAt) {
// We have a newer update() call running, so we can bail on this render.
return false;
}
}
this.shouldRenderAt = null;
this.renderedAt = Date.now();
await this.render();
this.hasRendered = true;
return true;
}

async render() {
Expand Down Expand Up @@ -113,5 +129,6 @@ export const getLive = (engines) => class BookshopLive {

core.graftTrees(startNode, endNode, output);
}
this.renderCount += 1;
}
}

0 comments on commit c34e2d4

Please sign in to comment.