-
Notifications
You must be signed in to change notification settings - Fork 27k
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
Get rid of eval #1611
Get rid of eval #1611
Conversation
lib/page-loader.js
Outdated
} | ||
|
||
return new Promise((resolve, reject) => { | ||
const fire = ({ error, page }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, this is cleaner using mitt
than doing it manually
server/index.js
Outdated
await this.hotReloader.ensurePage(pathname) | ||
} | ||
|
||
if (!this.handleBuildId(params.buildId, res)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense, however since buildId is in the URL segment, do you think it would make more sense to 404 on a buildId mismatch? Conceptually isn't a buildId mismatch just a missing resource? I think that would cause the loadScript
-> <script>
onerror to fire, and then your error listener would pick it up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bringking That's true.
But detecting 404 between browsers will be an issue.
This is a much better way and we know it's working 100% sure.
And we could improve this later on.
lib/router/router.js
Outdated
async fetchRoute (route) { | ||
// Wait for webpack to became idle if it's not. | ||
// More info: https://github.com/zeit/next.js/pull/1511 | ||
if (webpackModule && webpackModule.hot && webpackModule.hot.status() !== 'idle') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to do this inside the pageLoader before it run the code.
@@ -7,7 +7,7 @@ export default class JsonPagesPlugin { | |||
|
|||
pages.forEach((pageName) => { | |||
const page = compilation.assets[pageName] | |||
delete compilation.assets[pageName] | |||
// delete compilation.assets[pageName] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this plugin.
server/build/plugins/pages-plugin.js
Outdated
// TODO: We need to move "client-bundles" back to "bundles" once we remove | ||
// all the JSON eval stuff | ||
delete compilation.assets[chunk.name] | ||
compilation.assets[`client-bundles/pages/${pageName}.js`] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the "client-bundles" back to "bundles".
server/render.js
Outdated
@@ -105,26 +103,70 @@ async function doRender (req, res, pathname, query, { | |||
err: (err && dev) ? errorToJSON(err) : null | |||
}, | |||
dev, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need these anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
client/index.js
Outdated
const ErrorComponent = pageLoader.loadPageSync('/_error') | ||
let Component | ||
|
||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Load these in async and the initial scripts can be async.
client/index.js
Outdated
const Component = evalScript(component).default | ||
const ErrorComponent = evalScript(errorComponent).default | ||
const pageLoader = window.NEXT_PAGE_LOADER = new PageLoader(buildId) | ||
if (window.NEXT_LOADED_PAGES) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefix these globals with "__"
@@ -97,11 +97,14 @@ export class NextScript extends Component { | |||
|
|||
render () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not critical or a blocker for this approach to work, but I would love to see the addition of <link rel="preload">
to see if helps mitigate the added latency of downloading the components as script tags. Since we are not streaming the HTML, it might make negligible difference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't get this, could you give me more info?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you add a link tag with the attribute "preload" in the head, it gives the browser a hint that we are going to be loading some scripts later on in the document. This could possibly give us a slight boost in Time To Interactive. See this for some background. https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/
So in the _document, for the initial components (errorComponent and the current route bundle) you could render and possibly have them evaluated sooner. But we should profile it and see if it helps. Does that make more sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bringking I'll prefer using <script async />
even that article mentioned it's blocks.
In my tests and experience, browser will start painting the page before even the script hits the server.
(script async is not yet done)
But I'll do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, preload links would be helpful for any Link marked as "prefetch" since the browser can start those downloads right away. This possibility is now available since we are using plain JS (you can't preload JSON according to the spec)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bringking I tested with very low network connections and script async do fine.
Since link preload only chrome now and script async is doing great for us, I'm going to defer this to later.
We no longer need it.
Because that's the place to do it.
This is ready to 🚢 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not my place to approve, however in my testing this branch decreases our time-to-interactive by ~1-2 seconds on a 3G connection, since the HTML document is considerably smaller. And no un-safe eval to boot. Awesome job man!
Minor question - I noticed <Link prefetch>
results in a non-async script tag. Should these be async as well?
client/index.js
Outdated
const pageLoader = window.__NEXT_PAGE_LOADER__ = new PageLoader(buildId) | ||
if (window.__NEXT_LOADED_PAGES__) { | ||
window.__NEXT_LOADED_PAGES__.forEach((fn) => fn()) | ||
delete window.__NEXT_LOADED_PAGES__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this matters anymore, but at some point it was considered "bad" to use delete
since it is hard to optimize by V8 and other compilers. Whenever I see delete
, usually I think there might be a better way via re-assignment. Obviously not a blocker, just an observation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, delete is not bad. You need to use it wisely. Delete is bad when used inside a constructor. Because that's de-optimization. That's also valid for when we are creating a lot of objects from that class.
We need to use it wisely. And we need to delete when needed. Anyway, it's not needed here and having it also not a problem :)
@bringking good question. |
@arunoda this is awesome, any thoughts on when you guys can get this one out? |
@bringking this is high priority. Whenever @nkzawa can review this will ship |
server/render.js
Outdated
function loadPage () { | ||
var error = new Error('Page not exists: ${page}') | ||
error.pageNotFound = true | ||
error.statusCode = 404 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have two similar values ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good one. I think we can live with statusCode. I'll change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
throw new Error('Route name should start with a "/"') | ||
} | ||
|
||
return route.replace(/index$/, '') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/foo/index
and /foo
would become different routes. Is it intended ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. Both are the same. Need to fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
script.onerror = () => { | ||
const error = new Error(`Error when loading route: ${route}`) | ||
this.registerEvents.emit(route, { error }) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks we can ditch event emitter and make things simpler like the following.
return new Promise (resolve, reject) {
script.onload = () => {
const { page, statusCode } = window.__NEXT_PAGES_[route]
if (statusCode >= 400) {
const err = new Error()
err.statusCode = statusCode
reject(err)
return
}
// wait for webpack ready
resolve(page)
}
script.onerror = () => {
reject(const error = new Error(`Error when loading route: ${route}`))
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would make the script of page components simple as well, since it doesn't need to wait for pageLoader
ready.
const newContent = `
window.__NEXT_PAGES_[${JSON.stringify(routename)}] = {
statusCode: ${statusCode},
page: ${content}
}
`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Let's do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to keep the next pages inside a variable of PageLoader. Anyway doing following is not exactly possible.
const newContent = `
window.__NEXT_PAGES_[${JSON.stringify(routename)}] = {
statusCode: ${statusCode},
page: ${content}
}
`
That's because, webpack is not available when the page is loading at first.
But I did the next best thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can move values to PageLoader when it's ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what currently we are doing. Here's how this looks now:
const newContent = `
window.__NEXT_REGISTER_PAGE('${routeName}', function() {
var comp = ${content}
return { page: comp.default }
})
`
loadPage (route) { | ||
route = this.normalizeRoute(route) | ||
|
||
const cachedPage = this.pageCache[route] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks cache is not used when multiple concurrent requests happen for a route at a same time. maybe we can improve that later tho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's doable. I'll do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nkzawa actually concurrent request are handled here: https://github.com/zeit/next.js/pull/1611/files/866319c76d0fa80df68d7aca809f3af81c68e6b6#diff-c7ddf3ad46bcb0938d6043ea0393d554R48
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, had to change this with the new mitt()
less implementation.
@nkzawa updated based on your comments. |
Found a bug and fixing it. |
Had to came back to the Anyway, since we use |
Closing this PR since #1700 has it. |
Fixes #1301
Got a lot of ideas (almost all actually) from @bringking #1608
The difference is, I wanted to make this minimal as possible and incrementally integrate into Next.js