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

[RFC] Rendering in Next.js (SSR, Pre-rendering, CSR, SPA) #7355

Closed
timneutkens opened this issue May 16, 2019 · 18 comments · Fixed by #7293
Assignees
Labels
RFC

Comments

@timneutkens
Copy link
Member

@timneutkens timneutkens commented May 16, 2019

Next.js currently has 2 modes of rendering:

  • Dynamic rendering means render on demand when a request comes in.
  • Pre-rendering means rendering to html at build time using next export

These modes work fine if your application only has pages of a certain type. For example zeit.co/docs is completely static. However more often than not your application is not binary, and requirements for static rendering change over time.

For example, zeit.co has a blog with static content, marketing pages, a dashboard and more. In the current model, zeit.co would be deployed with the serverless target and every page becomes a serverless function, including the blog and marketing pages that can actually be generated at build time.

Furthermore we've seen a common pattern where you'd want the dashboard to be an appshell type application that only shows the header and a loading spinner in the pre-rendered response, then after hydrating the page the data is fetched and rendered to make the application feel faster. This is generally referred to as a client-side rendered application.

Since Next.js is in control of the compilation pipeline we can decide at build time if a page will always get the same result by inferring if the page has data requirements. If it doesn't have data requirements we'll automatically render the page as a static HTML file.

When this proposal is implemented you'll be able to choose between pre-rendering and dynamic rendering on the page level.

Goals

  • Provide fast by default experience
  • Optimize pages that we know are always going to render the same result
  • Allow usage of dynamic, static or client-side only on a per-page level
  • Support client-side only applications that lazy-load data
  • Powerful use-case for dashboards
  • Great for marketing pages that lazy-load login / authentication state

API

This proposal doesn't need any API changes, it's mostly related to changing the semantics and internals of Next.js to export HTML in certain cases during next build.

Initially, we will cover the case where getInitialProps is not defined. Meaning that if a page doesn't have getInitialProps it is automatically exported as HTML.

// pages/about.js
export default () => <p>Hello world</p>

This is always going to render the same, so during build, we export it to an HTML file.

We might also need to detect router usage, but we won't cover that in the initial implementation as it's not as common. In this case you probably don't want to do dynamic rendering.

import { useRouter } from 'next/router'

export default () => {
  const router = useRouter()
  const { query } = router
  
  return <p>Page: {query.page}</p>
}

On the Now side of things @now/next will be updated to upload the .html files generated by the build.

@developit

This comment has been minimized.

Copy link
Collaborator

@developit developit commented May 16, 2019

yay for more granular tradeoffs!

@amesas

This comment has been minimized.

Copy link

@amesas amesas commented May 16, 2019

+1 for "mixed case", the app shell pattern is very common for us.

@glenarama

This comment has been minimized.

Copy link

@glenarama glenarama commented May 16, 2019

I found this after discussing an issue on Spectrum about Auth.

If the app has centralised its page auth (Or other data centric logic) into the _app.js file this breaks things quite badly.

It should also check for getInitialProps in _app.js

@PullJosh

This comment has been minimized.

Copy link

@PullJosh PullJosh commented May 16, 2019

Can this work while using Apollo client (and other components that pull data)?

@gregberge

This comment has been minimized.

Copy link

@gregberge gregberge commented May 16, 2019

I think it is not the good approach. For now, yes you can know if a page requires data or not. But tomorrow with Suspense it will not be possible to know it before rendering it.

@lfades

This comment has been minimized.

Copy link
Member

@lfades lfades commented May 16, 2019

@glenarama if we check for a getInitialProps in _app.js then we will never have this setup working, because _app.js is applied to every page.

@PullJosh Yes and no, if you want SSR for components that are fetching data then you'll need to make sure of including a getInitialProps in the page, even if it's empty, so the page remains dynamic, in the other side if you are okay with no having SSR and do client-side fetching of data then just don't add getInitialProps to the page and your queries/mutations will continue to work as usual.

@lfades

This comment has been minimized.

Copy link
Member

@lfades lfades commented May 16, 2019

@neoziro You're right, suspense will change things but it's not yet out, this is just the initial implementation.

@revskill10

This comment has been minimized.

Copy link
Contributor

@revskill10 revskill10 commented May 16, 2019

Is there any chance to extract the NextJS compiler into its own webpack plugin ?
So that users could leverage NextJS compiler into his own apps without any lock-in.
One example: A plugin to compile entry points into serverless lambdas.

@possibilities

This comment has been minimized.

Copy link
Contributor

@possibilities possibilities commented May 17, 2019

I like the sound of this. I wonder if there's any solution, though it would probably require additions to the api, would allow me to still have a getInitialProps, but it would be invoked on the client when exported and things would otherwise behave the same as Ssr. Perhaps I could define path globs in next config to declare whats static? I imagine I can write once and choose how to deliver it separately. A use case might be shipping a static site to humans and server rendered site to robots.

@gregberge

This comment has been minimized.

Copy link

@gregberge gregberge commented May 17, 2019

@lfades the problem is that it is not possible to implement with Suspense. Except saying « we support Suspense, yes but please tell us that this page has no request » and it breaks all the flexibility of Suspense. I am just saying that including this kind of change could (in future) become a breaking change when you will have to remove it. It is just a « be careful » message.

@willowHR

This comment has been minimized.

Copy link

@willowHR willowHR commented May 17, 2019

@glenarama if we check for a getInitialProps in _app.js then we will never have this setup working, because _app.js is applied to every page.

I don't follow the reasoning - if you have a custom _app.js with a getInitialProps your injecting data into every page since it will wrap all pages. So this is the behaviour you want. If your custom _app.js doesn't have a getInitialProps your not injecting data globally so can revert to page level logic.

Personally I don't like this proposal (or the implementation) it adds a lot of dangerous edge cases to data caching which could introduce security issues down the road.

@timneutkens

This comment has been minimized.

Copy link
Member Author

@timneutkens timneutkens commented May 17, 2019

Note that if _app.js has a custom getInitialProps method we'll opt-out of this new behavior. Also note that this is the first iteration on the problem. We're going to incrementally add more.

it adds a lot of dangerous edge cases to data caching which could introduce security issues down the road.

Instead of saying "introduce security issues down the road" please give concrete
examples.

So that users could leverage NextJS compiler into his own apps without any lock-in.

First of all the Next.js compiler is not a webpack plugin, and it can't be one, as it does far more than just handle webpack and it could work without webpack.

Besides that you'd end up with a (most likely) worse implementation of just a fraction of the features Next.js has.

Re: Suspense. As said on twitter it's definitely possible. Can't share what I've been working on right now but already have a proof of concept.

@willowHR

This comment has been minimized.

Copy link

@willowHR willowHR commented May 17, 2019

Glad to hear about the _app.js opt out.

Regarding my security concern - I've walked through the concept in more detail and am less concerned. I do have some hooks based data in components - but I guess they should really be in getInitialProps so docs can cover this.

@timneutkens

This comment has been minimized.

Copy link
Member Author

@timneutkens timneutkens commented May 23, 2019

This is not completely implemented yet but #7293 is a start (the getInitialProps part).

@baer

This comment has been minimized.

Copy link

@baer baer commented May 28, 2019

I love the additional granularity in this proposal - it's a really elegant way to improve the so-called Documents to Applications Continuum. There will still be, from how I understand it, a gap in this proposal concerning routing for static pages in that you'll still need to define your redirects.

To take your example above of a client-side rendered application, which, with this RFP could be a page in a larger project, you'll need two things beyond triggering the static build.

  1. A server.js file to define the routing in dev mode
  2. A way to define the routing in your production environment whether that's Now Routes, a serve.json, Netlify redirects, or something else.

The way I've solved this is to create a single file called something like redirects.js, and a pair of files to generate config for the two environments:

redirects.js

module.exports = [
  { externalURL: `/customers/:customerId/order/:orderId`, staticPage: `/order` },
  { externalURL: `/customers/:customerId`, staticPage: `/customer` },
  { externalURL: `/customers`, staticPage: `/customers` }
]

server/index.js

...

// This allows the app to respond to the dynamic routes like `/posts/{postId}` and is the
// Next.js equivalent of the redirects in serve.json file. See scripts/build-serve-config.js
routes.forEach(route => {
  server.get(route.externalURL, (req, res) =>
    app.render(req, res, route.staticPage, req.params)
  )
})

...

build-serve-config.js

const fs = require(`fs`)
const path = require(`path`)
const redirects = require(`../redirects`)

const OUTPUT_PATH = path.join(__dirname, `../out`)
const FILE_NAME = `serve.json`

 const config = {
  renderSingle: true,
  trailingSlash: true,
  rewrites: redirects.map(redirect => ({
    source: redirect.externalURL,
    destination: redirect.staticPage
  }))
}

 fs.writeFile(
  path.join(OUTPUT_PATH, FILE_NAME),
  JSON.stringify(config, null, 2),
  err => {
    if (err) { throw err }
    console.log(`Generated: ${OUTPUT_PATH}  ${FILE_NAME}`)
  }
)

What I like about this RFP is that each page (which is sometimes a separate team) defines more of its own behavior. To take this concept further each page could define its own routing too. The problem is that most routing uses wildcards, and isn't flat which means route order matters. You can see in the example below how this would fall on its face if the Customer routes were defined first.

class Order extends Component {
  static routes = [
    `/customers/:customerId/order/:orderId`
    `/customers/:customerId/order`
  ]

  render () {
    return (
      <p>I'm a customer invoice</p>
    )
  }
}

Maybe Express router composition can be a source of inspiration. Another option, which is out of scope for this RFP, is to define Next.js routing with a separate config. I'm not sure what the best option is here.

@smithyj

This comment has been minimized.

Copy link

@smithyj smithyj commented Jun 13, 2019

Now, I use export to deploy and also have a timer to execute export, but getInitialProps won't execute again after opening the page. How can I get it to execute again? I don't want to do it again in componentDidMount

@Janpot

This comment has been minimized.

Copy link
Contributor

@Janpot Janpot commented Jul 27, 2019

I'd also be interested in prerendering dynamic routes, if that's feasable

// /pages/[dynamic]/some-page.jsx

export const config = {
  prerenderedRouteParams: {
    dynamic: [ 'value1', 'value2' ]
  }
}

export default function SomePage ({ data }) {
  return (
    <div>{data}</div>
  )
};

SomePage.getInitialProps = async ({ query }) {
  const data = await fs.readFile(`../data/${query.dynamic}.txt`)
  return { data };
};

Where the page would be prerendered twice, once for each of the values 'value1' and 'value2'.

Edit:

What I'm mainly looking for is a way to 'bake' translations in my localized application. I'd just like a way to inline i18n messages into my bundles instead of having to serve or include some sort of locale.json somewhere.

@timneutkens

This comment has been minimized.

Copy link
Member Author

@timneutkens timneutkens commented Aug 9, 2019

Going to close this RFC as it was landed in Next.js 9: nextjs.org/blog/next-9.

There's a follow-up RFC coming for dynamic rendering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.