-
-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prevent Route Flickers #364
Conversation
- was always using the `require` built-in
- dispatch loading & loaded events - track `ready` state value - always use `child.default` key - add `componentDidUpdate` for final event - only render _if_ there is a child to render
- use initial render to kick-off async loads - manage `inProgress` as escape-hatch var - once “loaded” event fires, render to real tree
I still don't know how I feel about mounting the app twice.. I can imagine some people thinking they can get away with some side effect that should really only ever run once, and then it runs twice and they get completely confused. Nevertheless, I do think that the problem edge case (double-mount issue) is less common and less important than the case being fixed (always-flickering-in-every-prerendered-app). If there's a FAQ somewhere, maybe we should add a "My app's componentDidMount() is running twice! Why?" And BTW, you mentioned this fixes a bug in emerging markets. That's true, but in reality this does effect every network speed. The flicker effect that this fixes shows up even on fast connections. On fast Wi-Fi, you still see 50-150ms of flicker in between "app shell load & render" (flicker) "home render" (end flicker). All that to say it's an even more important fix in that it effects all users. |
Yeah, I realize I was lucky enough to rarely see the flicker before your post. When I did see it, I thought it was a font-load issue or something. The alternative to the "double" mounting is to create a built-in Ideally, there should be a Webpack-runtime function we can call pre-mount to know what needs to be loaded.... Maybe it exists? That would make this whole thing a breeze! function init() {
// basic init
}
function boot() {
loadChunks().then(init);
} |
Your idea of the That might be the best solution... and maybe it's simple. It might be as simple as having the app root element do the following... function AppRoot ({ isLoading, children }) {
const props = isLoading ? {} : {
dangerouslySetInnerHTML: appRootElement.innerHTML
}
props.children = children
return h('div', props)
}
The real question here is, if we set both dangerouslySetInnerHTML props and children props, what happens? I'll give this a try tomorrow and see... |
Good news, looks like that approach might work (check the console): https://codesandbox.io/s/0o8jjvz7vv You can This means the app could be rendered, and the HTML set to the prerendered value. 😄 |
Hey @ajoslin, I had similar at one point. Your one-off demo works, but it's not actually casting an Maybe I'm missing something, but here's how the flow would work with
Now, if you mounted to a throwaway <div dangerouslySetInnerHTML={{ __html:props.html }} /> ... you would never initiate |
Alright, so I tried out the We should either go with your first solution or try a couple more things to try to get around the double mount. Another solution is possibly to add a wrapper node around each route component with a deterministic selector. The prerender would then look like: <div id="app">
App Content
<div class="route-wrapper" path="/">
<div>Home Route Content</div>
</div>
</div> Then AsyncComponent could receive the path which it's prerendering as a prop (this is possible, I believe), and based upon the innerHTML of AsyncComponent.prototype.componentWillMount = () => {
const prerenderedEl = document.querySelector(`.route-wrapper[path="${this.props.path}"]`)
if (prerenderedEl) {
this.prerenderedHtml = prerenderedEl.innerHTML
}
}
AsyncComponent.prototype.render = (state, props) => {
// Now AsyncComponent will render a Vdom version of the prerendered route wrapper el...
const parentProps = {
path: props.path,
class: 'route-wrapper'
}
if (state.child) {
parentProps.children = h(state.child, props)
} else {
parentProps.dangerouslySetInnerHTML = {__html: this.prerenderedHtml}
}
return h('div', parentProps)
} Finally, during prerender we need to render these Would it be possible to do this with Webpack, by patching all requires in This HOC would render |
Maybe. I still believe the true answer lies within Webpack since it's the master-orchestrator of what loads, when it loads, and when it's done. Preact does it's job perfectly as it is, shouldn't be fighting it. We need some type of Manifest Plugin that maps pathnames to // ignore *.js.map files here
var mapping = {
'/': ['route-home'],
'/about': ['route-about']
}
Then the const { h, render } = require('preact');
// ...
function getApp() {
let x = require('preact-cli-entrypoint');
return h(x && x.default || x);
}
function init() {
let app = getApp();
root = render(app, body, root);
}
const mapping = { ... };
let chunks = mapping[location.pathname] || [];
if (chunks.length > 0) {
// will be handled by webpack @ runtime
require.ensure(chunks, init);
} else {
init();
}
module.hot && module.hot.accept('preact-cli-entrypoint', init); |
Yeah, the compile time way really does sound the most reliable. The question is how to do it. One way that might be possible with prerender:
The downside is this would only work if the routing library people use follows Do you think this approach would work? |
Webpack already splits each The above code snippet I wrote will work. I've been with family all day today, but will start back up with this now. It's just a matter of printing the Also, it would need to be an exact match for the routes. Partly to enforce static-routes only && partly because |
Random thoughts:
|
Any progress update on this? I'm also experiencing the same issue where the page flickers on a slow connection. |
I have a possible solution I can PR. |
@developit Did you have that sumthin' sumthin'? |
I got it 🍾 I think I'll open a new PR instead of updating this one~ |
@lukeed my $100 bounty is still open on this too! 🍾 |
This approach reworks the
preact-cli-entrypoint
&AsyncComponent
so that the initial client-side mount won't wipe/reset the existing static/prerendered markup while waiting for chunks to finish loading.Currently, the first bundle load kicks off the webpack instance (which initiates the Async loads). That sounds great (and it is!) unless you're on a bad connection and/or are in an emerging market. In those cases, what looked like a functional, ready page, will disappear (to your surprise) and you'll have to wait through another 5+ seconds until the app is back "online"
With this, the first bundle load kicks off the webpack instance (which still initiates the Async loads). The difference here is that it's now "rendering" to a cloned copy of
document.body
, but nothing here will affect the page. Its main purpose is to begin the async chunk-loading.Once the chunk(s) have been fully loaded, a second render will take place, targeting the real
document.body
. In basic cases this will re-create the same markup, so when Preact enters the diff-phase, no changes are made. However, Preact now has full control of the client-side application, allowing libs likepreact-router
to take hold.As @ajoslin pointed out, this approach does fire two sets of side effects (eg; a network request during
componentWillMount
x2), but anything attached to the fake/clonedocument.body
will not yield any true visual effect. Also, this only applies to the very first load, since all other routes (eg,/
==>/about
) are handled on the client-side only without any type of static-vs-jsx negotiation.Sorry for the wall of text ~ 🙇
TL;DR:
Closes #281. Big thanks to @ajoslin for getting started on this.