diff --git a/javascript-modules/generate/lib/live-connector.js b/javascript-modules/generate/lib/live-connector.js
index b3fd15e9..d9ff6f31 100644
--- a/javascript-modules/generate/lib/live-connector.js
+++ b/javascript-modules/generate/lib/live-connector.js
@@ -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();
diff --git a/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature b/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature
index a9225123..711780be 100644
--- a/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature
+++ b/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature
@@ -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:
diff --git a/javascript-modules/integration-tests/features/live/bookshop_live_performance.feature b/javascript-modules/integration-tests/features/live/bookshop_live_performance.feature
new file mode 100644
index 00000000..98ccca74
--- /dev/null
+++ b/javascript-modules/integration-tests/features/live/bookshop_live_performance.feature
@@ -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:
+ """
+
{{ .title }}
+ """
+ * a site/layouts/index.html file containing:
+ """
+
+
+ {{ partial "bookshop_bindings" `(dict "title" .Params.block.title)` }}
+ {{ partial "bookshop" (slice "single" (dict "title" .Params.block.title)) }}
+
+
+ """
+ * [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
diff --git a/javascript-modules/integration-tests/support/steps.js b/javascript-modules/integration-tests/support/steps.js
index 7a5a2db5..09e76d50 100644
--- a/javascript-modules/integration-tests/support/steps.js
+++ b/javascript-modules/integration-tests/support/steps.js
@@ -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(), []);
});
@@ -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`);
diff --git a/javascript-modules/live/lib/app/live.js b/javascript-modules/live/lib/app/live.js
index a7d8a515..533dc84a 100644
--- a/javascript-modules/live/lib/app/live.js
+++ b/javascript-modules/live/lib/app/live.js
@@ -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));
}
@@ -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)
@@ -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() {
@@ -113,5 +129,6 @@ export const getLive = (engines) => class BookshopLive {
core.graftTrees(startNode, endNode, output);
}
+ this.renderCount += 1;
}
}