Prerendering #3359
Prerendering #3359
Conversation
| usablePerfObj = { | ||
| now() {}, | ||
| mark() {} | ||
| }; |
dmose
Sep 6, 2017
Member
I'm assuming this is needed so that it runs under node. A comment indicating that would nice.
I'm assuming this is needed so that it runs under node. A comment indicating that would nice.
| @@ -1,4 +1,4 @@ | |||
| module.exports = { | |||
| // constant to know if the page is about:newtab or about:home | |||
| IS_NEWTAB: document.documentURI === "about:newtab" | |||
| IS_NEWTAB: global.document && global.document.documentURI === "about:newtab" | |||
dmose
Sep 6, 2017
Member
What is it that keeps this code running in the browser? Webpack magic? Do we assign global=Window somewhere?
What is it that keeps this code running in the browser? Webpack magic? Do we assign global=Window somewhere?
k88hudson
Sep 6, 2017
Author
Member
global is window in webpack, yes
global is window in webpack, yes
| @@ -22,6 +23,9 @@ function addLocaleDataForReactIntl({locale}) { | |||
|
|
|||
| class Base extends React.Component { | |||
| componentDidMount() { | |||
| // Request state after the first render | |||
| this.props.dispatch(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST})); | |||
|
|
|||
dmose
Sep 6, 2017
Member
A comment explaining why we do this extra round trip (e.g. for pre-rendering), perhaps even in a bit of details, would likely be useful to future code readers.
A comment explaining why we do this extra round trip (e.g. for pre-rendering), perhaps even in a bit of details, would likely be useful to future code readers.
dmose
Sep 6, 2017
Member
A unit test for this would be good too; https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md has an example using sinon that should be similar.
A unit test for this would be good too; https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md has an example using sinon that should be similar.
| @@ -15,7 +15,7 @@ this.NewTabInit = class NewTabInit { | |||
| onAction(action) { | |||
| let newAction; | |||
| switch (action.type) { | |||
| case at.NEW_TAB_LOAD: | |||
| case at.NEW_TAB_STATE_REQUEST: | |||
dmose
Sep 6, 2017
Member
This wants a unit test, I think. Maybe a look at the code coverage numbers would show other changes that want one too?
This wants a unit test, I think. Maybe a look at the code coverage numbers would show other changes that want one too?
k88hudson
Sep 7, 2017
Author
Member
Currently this file isn't being tested (which is an oversight), do you mind if I file a follow-up for this?
Currently this file isn't being tested (which is an oversight), do you mind if I file a follow-up for this?
dmose
Sep 7, 2017
Member
Because of the high-value in getting this into the tree sooner rather than later, yes.
Because of the high-value in getting this into the tree sooner rather than later, yes.
|
|
||
| function templateJs(state) { | ||
| return `// Note - this is a generated file. | ||
| window.gActivityStreamPrerenderedState = ${JSON.stringify(state, null, 2)}; |
dmose
Sep 6, 2017
Member
This wants a comment describing what it's doing and why. Probably in the rendered file so that people who find that artifact quickly see what is.
This wants a comment describing what it's doing and why. Probably in the rendered file so that people who find that artifact quickly see what is.
| `; | ||
| } | ||
|
|
||
| const args = require("minimist")(process.argv.slice(2), {alias: {baseUrl: "b", title: "t", locale: "l", direction: "d"}}); |
dmose
Sep 6, 2017
Member
This is a bit hard to skim. Maybe break it up into two lines, and explain the magic constant of 2 in a comment?
This is a bit hard to skim. Maybe break it up into two lines, and explain the magic constant of 2 in a comment?
| fs.writeFileSync(HTML_FILE_PATH, templateHTML(options)); // eslint-disable-line no-sync | ||
| fs.writeFileSync(PRERENDERED_HTML_FILE_PATH, templateHTML(options, html)); // eslint-disable-line no-sync | ||
| fs.writeFileSync(INITIAL_STATE_JS_FILE_PATH, templateJs(state)); // eslint-disable-line no-sync | ||
|
|
dmose
Sep 6, 2017
Member
It'd improve readaibility to just put /* eslint-disable no-sync */ at the top of the file.
It'd improve readaibility to just put /* eslint-disable no-sync */ at the top of the file.
| // Note: the key on IntlProvider must be static in order to not blow away | ||
| // all elements on a locale change (such as after preloading). | ||
| // See https://github.com/yahoo/react-intl/issues/695 for more info. | ||
| return (<IntlProvider key="STATIC" locale={locale} messages={strings}> |
Mardak
Sep 6, 2017
•
Member
This change seems to cause the search placeholder and migration texts to not appear on the first about:home. (You can also test with ./mach run about:newtab) Changing back to key={locale} avoids this:

My guess is that LOCALE_UPDATED happens at an unexpected time? Also, guessing that TOP SITES gets the string because TopSites gets re-render-ed.
This change seems to cause the search placeholder and migration texts to not appear on the first about:home. (You can also test with ./mach run about:newtab) Changing back to key={locale} avoids this:

My guess is that LOCALE_UPDATED happens at an unexpected time? Also, guessing that TOP SITES gets the string because TopSites gets re-render-ed.
Mardak
Sep 6, 2017
Member
Looks like passing in a dummy locale prop, e.g., <Search locale={locale} />, makes it render again
Looks like passing in a dummy locale prop, e.g., <Search locale={locale} />, makes it render again
|
I pushed to talos moving the activity-stream-prerendered.html to activity-stream.html: Nothing significant so far although tabpaint shows 6% improvement but low confidence after 10 before and 10 after runs. |
| <body class="activity-stream"> | ||
| <div id="root"></div> | ||
| <div id="snippets-container"> | ||
| <div id="snippets"></div> |
Mardak
Sep 7, 2017
Member
We'll still need these snippets related div#ids, yeah?
We'll still need these snippets related div#ids, yeah?
|
Still nothing significant (and maybe even a slight tart regression?) I also pushed to try with aboutHome enabled: |
| <link rel="icon" type="image/svg+xml" href="${options.baseUrl}img/newtab-icon.svg"> | ||
| </head> | ||
| <body> | ||
| <div id="root">${isPrerendered ? html : ""}</div>${isPrerendered ? ` |
dmose
Sep 7, 2017
Member
Having so much of the semantics come from punctuation, and specifically nested template strings, makes this a bit hard to follow. Pull out the <script> for stream-initial-state into a const higher up in the script?
Having so much of the semantics come from punctuation, and specifically nested template strings, makes this a bit hard to follow. Pull out the <script> for stream-initial-state into a const higher up in the script?
| @@ -0,0 +1,59 @@ | |||
| const fs = require("fs"); | |||
| const path = require("path"); | |||
| const prerender = require("./prerender"); | |||
dmose
Sep 7, 2017
Member
Add a comment here about what this is and how it's built?
Add a comment here about what this is and how it's built?
dmose
Sep 7, 2017
Member
Great!
Great!
| `; | ||
| } | ||
|
|
||
| function templateJs(state) { |
dmose
Sep 7, 2017
Member
JSDoc here for first-time readers would be helpful, mostly for the info about "state"
JSDoc here for first-time readers would be helpful, mostly for the info about "state"
dmose
Sep 7, 2017
Member
Very nice; thanks!
Very nice; thanks!
| Object.keys(allStrings["en-US"]).forEach(key => { | ||
| strings[key] = " "; | ||
| }); | ||
| } |
k88hudson
Sep 7, 2017
Author
Member
I think string fallbacks work decently well at the moment (they fall back to English for any missing strings), although there are some cases where dynamic string ids get injected (e.g. TopStories) that could be handled better on a case-by-case basis
I think string fallbacks work decently well at the moment (they fall back to English for any missing strings), although there are some cases where dynamic string ids get injected (e.g. TopStories) that could be handled better on a case-by-case basis
| topics: [{name: "Must Reads", category: "World", url: "https://getpocket.com/explore/must-reads?src=ff_new_tab"}] | ||
| } | ||
| }); | ||
|
|
dmose
Sep 7, 2017
Member
Since each of the above dispatches effectively replaces ones that currently come from chrome, but have a comment above each one talking about how they differ or any assumptions in the interest of future code maintainability.
Since each of the above dispatches effectively replaces ones that currently come from chrome, but have a comment above each one talking about how they differ or any assumptions in the interest of future code maintainability.
dmose
Sep 7, 2017
Member
Particularly keeping in mind that effectively this code may also need to be updated as the chrome code changes over time.
Particularly keeping in mind that effectively this code may also need to be updated as the chrome code changes over time.
k88hudson
Sep 7, 2017
Author
Member
It's not really so much that it comes from chrome as that specific actions are needed to set properties in certain reducers
It's not really so much that it comes from chrome as that specific actions are needed to set properties in certain reducers
k88hudson
Sep 7, 2017
Author
Member
I can actually add tests to help with this
I can actually add tests to help with this
dmose
Sep 7, 2017
Member
That sounds good.
That sounds good.
| @@ -0,0 +1,63 @@ | |||
| const React = require("react"); | |||
dmose
Sep 7, 2017
Member
This file definitely wants unit tests.
This file definitely wants unit tests.
k88hudson
Sep 7, 2017
Author
Member
Done!
Done!
|
|
||
| new DetectUserSessionStart().sendEventOrAddListener(); | ||
|
|
||
| const store = initStore(reducers); | ||
| const store = initStore(reducers, global.gActivityStreamPrerenderedState); |
dmose
Sep 7, 2017
Member
The changes to this file want unit tests too.
The changes to this file want unit tests too.
| @@ -49,20 +49,23 @@ const messageMiddleware = store => next => action => { | |||
| * @param {object} reducers An object containing Redux reducers | |||
dmose
Sep 7, 2017
Member
There should probably be one or two unit tests added here.
There should probably be one or two unit tests added here.
| const prefConfig = { | ||
| // Prefs listed with "invalidates: true" will prevent the prerendered version | ||
| // of AS from being used if their value is something other than what is listed | ||
| // here. |
dmose
Sep 7, 2017
Member
As we were discussing yesterday, some of these have minor consequences, which we currently believe are acceptable. Documenting those consequences and tradeoffs here would help maintainability.
As we were discussing yesterday, some of these have minor consequences, which we currently believe are acceptable. Documenting those consequences and tradeoffs here would help maintainability.
| feed.onPrefChanged("foo123", true); | ||
| assert.notCalled(feed._prefs.set); | ||
| }); | ||
| it("should set the prerender pref if one of the prefs in PrerenderData.invalidatingPrefs is changed", () => { |
dmose
Sep 7, 2017
Member
This wants to be split into two tests for the true -> false change and the false -> true change.
This wants to be split into two tests for the true -> false change and the false -> true change.
| @@ -1,3 +1,4 @@ | |||
| /* eslint-disable no-sync */ /* eslint-disable no-sync */ /* eslint-disable no-sync */ | |||
dmose
Sep 7, 2017
Member
Three times? :-)
Three times? :-)
| @@ -82,5 +82,5 @@ class Search extends React.Component { | |||
| } | |||
|
|
|||
| // initialized is passed to props so that Search will rerender when it receives strings | |||
dmose
Sep 7, 2017
Member
This comment needs to be updated too.
This comment needs to be updated too.
| @@ -29,6 +29,7 @@ const DEFAULT_OPTIONS = { | |||
| */ | |||
| function templateHTML(options, html) { | |||
| const isPrerendered = !!html; | |||
| const scriptForPrerendering = `\n<script src="${options.baseUrl}data/content/activity-stream-initial-state.js"></script>`; | |||
dmose
Sep 7, 2017
Member
nit: It'd be even more readable if you just embedded a new line in the string rather than using /n.
nit: It'd be even more readable if you just embedded a new line in the string rather than using /n.
| @@ -44,8 +45,7 @@ function templateHTML(options, html) { | |||
| <div id="root">${isPrerendered ? html : ""}</div> | |||
| <div id="snippets-container"> | |||
| <div id="snippets"></div> | |||
| </div>${isPrerendered ? ` | |||
| <script src="${options.baseUrl}data/content/activity-stream-initial-state.js"></script>` : ""} | |||
| </div>${isPrerendered ? `${scriptForPrerendering}` : ""} | |||
dmose
Sep 7, 2017
Member
Much nicer; thanks!
Much nicer; thanks!
|
Ok, I filed a bunch of follow-ups and added them to this meta-bug: #3372 |
|
Filed a bugzilla ticket for the aboutNewTabService changes that need to land in mc: |
| }); | ||
| it("should start uninitialized", () => { | ||
| const store = prerenderStore(); | ||
| const state = store.getState(); |
dmose
Sep 7, 2017
Member
In all of these tests, calling getState wants to be part of the verification block, not the execution block.
In all of these tests, calling getState wants to be part of the verification block, not the execution block.
|
Ok, I added three more commits;
|
|
Do we need/want to run this through Talos again, given that the performance characteristics have probably been changed a bit by the race fixes? |
|
This will need a patch to keep pine / m-c happy: diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -88,4 +88,8 @@ var whitelist = [
{file: "resource://app/modules/NewTabWebChannel.jsm"},
+ // browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
+ // will be used very soon by bug 1397875
+ {file: "resource://activity-stream/data/content/activity-stream-prerendered.html"},
+
// layout/mathml/nsMathMLChar.cpp
{file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"}, |
|
Looks like this test is failing with this PR: Some brief debugging points to it waiting for and never getting content search events: It's failing pretty much everywhere: https://treeherder.mozilla.org/#/jobs?repo=try&revision=1d54a70adfbd8b9f71db2a7062b9a9899725c70c&filter-searchStr=(bc&filter-resultStatus=testfailed&selectedJob=129397977 |
|
Weird ok, I'll take a look |
| - } | ||
| - }); | ||
| - }); | ||
| + await ContentTaskUtils.waitForCondition(() => content.wrappedJSObject.gContentSearchController && |
dmose
Sep 8, 2017
Member
It'd be more readable if each of the "content.wrappedJSObject" clauses had its own line, eslint-permitting.
It'd be more readable if each of the "content.wrappedJSObject" clauses had its own line, eslint-permitting.
dmose
Sep 8, 2017
Member
The original style of test seems more robust, though I get that it wasn't quite right. This fix seems ok for now, but I'd suggest also adding an XXX comment suggesting adding an event to the code or some such so that the test depends less on implementation details and is more useful for refactoring.
The original style of test seems more robust, though I get that it wasn't quite right. This fix seems ok for now, but I'd suggest also adding an XXX comment suggesting adding an event to the code or some such so that the test depends less on implementation details and is more useful for refactoring.
k88hudson
Sep 8, 2017
Author
Member
I don't really see how this relies more on implementation details, given that that particular event has to be fired (and also in a particular order, i.e. after the listener is added in the test). Additionally, I kind of feel like using the event makes this test way harder to debug/step through 😜
I don't really see how this relies more on implementation details, given that that particular event has to be fired (and also in a particular order, i.e. after the listener is added in the test). Additionally, I kind of feel like using the event makes this test way harder to debug/step through
dmose
Sep 8, 2017
Member
OK, that's a reasonable enough rationale.
OK, that's a reasonable enough rationale.
|
The aboutNewTabService patch to firefox.js seems to have bit-rotted. Re-create it, using -U2 so that it will be more robust and likely to last a bit longer? |
This prevents the checksum from failing for prerendering due to the state in the initial render being different than what it expects.
|
There's a similar browser_google_behavior that has a "promiseContentSearchReady" but isn't failing ? |
|
r=dmose |
|
I reverted with ade8033 part of 84534e4 that added |
This patch adds prerendering in a few commits:
globalfor globals, etc.)NEW_TAB_STATE_REQUEST, instead ofNEW_TAB_LOAD. This is needed for prerendering in order to ensure the first render matches the prerendered state.buildmc:htmltask, and all the basic prerendering tasks.aboutNewTabServiceto make use of this.<Topics />to allow it to render but be empty. This is to ensure things don't move around when the pre-rendered version switches to React.