Next.js allows you to leverage the simplicity of the React's component system
And makes it easy-to-use, amplifying its reach
It all started with a paradox
Paradox A: Hacker News
Why are we all sharing cutting edge JS frameworks on a 1990s website 🤔?
No JS, no SPA, no framework rewrite every month?
A: Because it works!
It loads fast. It streams HTML from the server from an in-memory cache of links
Building HN was probably easy and simple
And therefore, it's survived and thrived
We wanted our solution to be able to go up and down the power spectrum
I should be able to render simple websites or powerful dynamic and realtime apps…
With one universal programming model (React) and language (JS)
Meet next-news.now.sh
Paradox B: why is this deck written in Keynote?
A: it's not! It's a Next.js deck with SSR and MDX 😎
Next.js has been in production at zeit.co for almost two years
I'll share some of the principles, patterns, challenges and future directions
Minimal Convention over Configuration
sophisticated applications have many variable entry points
loading more code than necessary slows down download and initialization
This is a widely applicable notion
Code splitting is useful even for CLI applications!
Each sub-command is a different entry-point
Code-splitting fixes the SPA Syndrome:
Downloading the code for the Settings section when all you wanted was the About page
Convention: In Next.js, every entry point (top-level component) lives inside `./pages`
It naturally leads to simple project exploration
Emergent Convention
pages/ top level components (entry points) lib/ data fetching and tranformation helpers components/ standalone and "pure" components
What are some of the consequences of this convention?
Scalable for teams: your components usage on `./pages/x`
doesn't affect mine on `./pages/b`
Scalable for teams: distinction of "top-level" vs "building block" components leads to natural style guides
i.e.: Sketch.app in your browser
Top level components define their data needs via an extra lifecycle hook `getInitialProps`
You can decide how much or how little of the page to render
How do you decide?
A: "what's the critical datum for the entry point?"
next-news: `/news`
should display… the latest news
next-news: how about `/item?id=13850964`
?
A2: "Can my server answer quickly?"
e.g.: Instagram `/explore`
page
In other words: whose spinner do you want the user to see? The User-Agent's or yours?
Pre-rendering the layout is typically a better indication of progress
In addition, sometimes data can be loaded from an offline client cache and be best-effort invalidated
Other entry points can be prefetched via `<Link prefetch>`
`<Link>`
allows for page transitions without losing client state
Example: authentication. Load user once, maintain global state, don't refetch, sync upon change
With prefetching + SSR, we get the UX of a SPA with the initial perf of a 1990s website 🎉
By default each entry point maps to a route.
e.g.: `/a`
renders `./pages/a.js`
However, the server can match any incoming URL to any entrypoint
Design Constraint: the client shouldn't need to load the entire route map ahead of time
instagram.com/p/BRbwNt9hEiV → `/photo?id=BRbwNt9hEiV`
`<Link href="/photo?id=BRbwNt9hEiV" as="/p/BRbwNt9hEiV">Hi</Link>`
<Link href="" as=""> Router.push(href, as) <Link href="" as="" replace> Router.replace(href, as) <Link href="" prefetch> Router.prefetch(href)
We build on top of Webpack, which by default watches the entire workdir and re-builds everything
As projects grow in size and number of components, the situation is unsustainable
The situation is worsened when you're editing critical common components
Next.js now automatically subscribes to hot-reloading only the entry points open during dev
This improved iteration performance by 10x-15x for our codebase
Since the system loads components lazily, how do we deal with a changing codebase?
Answer: content-addressable build artifacts
`<Link href="/a">`
~=
`<Link href="/3f786850e387550fdab836ed7e6dc881de23001b/a.js">`
If the script fails to load due to a build mismatch,
a full page transition is performed
It's possible to maintain a global client store. The server can `await`
its population
export default connect(state => state)(({ title, linkTo, lastUpdate, light }) => { return ( <div> <h1>{title}</h1> <Clock lastUpdate={lastUpdate} light={light} /> <nav> <Link href={linkTo}><a>Navigate</a></Link> </nav> </div> ) })
Check out the `examples`
folder on GitHub:
./examples/data-fetch ./examples/with-redux ./examples/with-mobx ./examples/with-refnux ./examples/with-apollo
Easiest and most exportable way: inline styles
export default () => ( <div style={styles.main}> Hello World </div> ) const styles = { main: { color: blue } }
Main problem: it's only a strong subset of CSS
No media queries, animations, font declarations, hover states, pseudo elements…
Those are just symptoms. Biggest problem: inhibits co-evolution with CSS
CSS continues to evolve, we should not disregard it
Meet `<style jsx>`
Ahead-of-time Style Isolation via a Babel Transformation
export default () => ( <div> <p>woot</p> <style jsx>{` p { color: red; } `}</style> </div> )
Offered as a default, but not prescriptive.
Attempts to solve the "global CSS injection" problem
Out of the box, `<style jsx>`
dedupes (only one underlying element), detaches & server-renders
./examples/basic-css ./examples/with-glamor ./examples/with-aphrodite ./examples/with-styled-components ./examples/with-fela ./examples/with-cxs ./examples/with-styletron ./examples/with-global-stylesheet ./examples/with-scoped-stylesheets-and-postcss ./examples/with-styled-jsx-postcss
Next.js itself is a babel preset: `next/babel`
You can arbitrary and seamlessly extend Babel settings
`.babelrc`
{ "presets": ["next/babel"], "plugins": [ "babel-plugin-preval" ] }
`pages/index.js`
const greeting = preval` const fs = require('fs') module.exports = fs.readFileSync(require.resolve('./greeting.txt'), 'utf8') ` export default () => <div>{greeting}</div>
./examples/with-custom-babel-config ./examples/with-flow ./examples/with-universal-configuration
const express = require('express') const next = require('next') const dev = process.env.NODE_ENV !== 'production' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare() .then(() => { const server = express() server.get('/a', (req, res) => { return app.render(req, res, '/b', req.query) }) server.get('/b', (req, res) => { return app.render(req, res, '/a', req.query) }) server.get('*', (req, res) => { return handle(req, res) }) server.listen(3000, (err) => { if (err) throw err console.log('> Ready on http://localhost:3000') }) })
This API allows you to cache easily at the top level
./examples/custom-server ./examples/custom-server-koa ./examples/custom-server-express ./examples/custom-server-hapi ./examples/using-inferno ./examples/using-preact ./examples/using-router ./examples/with-next-routes ./examples/ssr-caching
A frequently overlooked aspect of web services and web sites is how naturally composable and scalable they are.
Split Next.js applications into smaller pieces
Scalable for teams: Smaller surface area
Scalable for teams: Shipped independently
Ongoing Work and Future Directions
Faster bootup times
`next-server`
Compiling less
Performance optimations to Webpack
Find bugs faster
Immediately see the affected source lines when an error is thrown