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 · 19 comments · Fixed by #7293
Closed

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

timneutkens opened this issue May 16, 2019 · 19 comments · Fixed by #7293
Assignees

Comments

@timneutkens
Copy link
Member

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

yay for more granular tradeoffs!

@amesas
Copy link

amesas commented May 16, 2019

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

@ghost
Copy link

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

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

@gregberge
Copy link

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

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

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

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

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

@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.

@ghost
Copy link

ghost 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
Copy link
Member Author

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.

@ghost
Copy link

ghost 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
Copy link
Member Author

timneutkens commented May 23, 2019

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

@baer
Copy link

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

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

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
Copy link
Member Author

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.

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 31, 2022
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 a pull request may close this issue.