Skip to content
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

Closed
wants to merge 26 commits into from
Closed

Get rid of eval #1611

wants to merge 26 commits into from

Conversation

arunoda
Copy link
Contributor

@arunoda arunoda commented Apr 4, 2017

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

}

return new Promise((resolve, reject) => {
const fire = ({ error, page }) => {

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)) {
Copy link

@bringking bringking Apr 4, 2017

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.

Copy link
Contributor Author

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.

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') {
Copy link
Contributor Author

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]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this plugin.

// 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`] = {
Copy link
Contributor Author

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,
Copy link
Contributor Author

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.

Copy link
Contributor Author

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 {
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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 () {
Copy link

@bringking bringking Apr 5, 2017

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

Copy link
Contributor Author

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?

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?

Copy link
Contributor Author

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.

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)

Copy link
Contributor Author

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.

@arunoda
Copy link
Contributor Author

arunoda commented Apr 6, 2017

This is ready to 🚢

Copy link

@bringking bringking left a 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?

screen shot 2017-04-06 at 1 23 06 pm

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__
Copy link

@bringking bringking Apr 6, 2017

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

Copy link
Contributor Author

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 :)

@arunoda
Copy link
Contributor Author

arunoda commented Apr 7, 2017

@bringking good question.
Seems like browsers do it automatically for us: http://stackoverflow.com/questions/3408805/is-the-async-attribute-property-useful-if-a-script-is-dynamically-added-to-the?answertab=active#tab-top

@bringking
Copy link

@arunoda this is awesome, any thoughts on when you guys can get this one out?

@rauchg
Copy link
Member

rauchg commented Apr 10, 2017

@bringking this is high priority. Whenever @nkzawa can review this will ship

@rauchg rauchg requested a review from nkzawa April 10, 2017 05:44
server/render.js Outdated
function loadPage () {
var error = new Error('Page not exists: ${page}')
error.pageNotFound = true
error.statusCode = 404
Copy link
Contributor

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 ?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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$/, '')
Copy link
Contributor

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 ?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 })
}
Copy link
Contributor

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}`))
  }
}

Copy link
Contributor

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}
  }
`

Copy link
Contributor Author

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.

Copy link
Contributor Author

@arunoda arunoda Apr 11, 2017

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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]
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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.

@arunoda
Copy link
Contributor Author

arunoda commented Apr 11, 2017

@nkzawa updated based on your comments.

@arunoda arunoda changed the title Get rid of eval [WIP] Get rid of eval Apr 11, 2017
@arunoda
Copy link
Contributor Author

arunoda commented Apr 11, 2017

EDIT: Fixed the bug.

Found a bug and fixing it.

@arunoda
Copy link
Contributor Author

arunoda commented Apr 11, 2017

Had to came back to the mitt() based page loader. If we are rely on script.onload, we need to maintain few other states as we are doing some async ops.

Anyway, since we use mitt() in other places, this is fine.

@arunoda arunoda changed the title [WIP] Get rid of eval Get rid of eval Apr 11, 2017
@arunoda
Copy link
Contributor Author

arunoda commented Apr 18, 2017

Closing this PR since #1700 has it.

@arunoda arunoda closed this Apr 18, 2017
@arunoda arunoda deleted the no-eval branch April 18, 2017 13:39
@lock lock bot locked as resolved and limited conversation to collaborators Jan 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants