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

feat(gatsby): SSR pages during development #27432

Merged
merged 149 commits into from Nov 17, 2020
Merged

feat(gatsby): SSR pages during development #27432

merged 149 commits into from Nov 17, 2020

Conversation

KyleAMathews
Copy link
Contributor

@KyleAMathews KyleAMathews commented Oct 13, 2020

related: #25729

  • SSR pages
  • show error page when SSR fails
  • hot reload on code changes
  • wait for waiting state
  • write util to compare dev vs prod ssr (filter out js script I think) & have integration test use it (also add some SSR plugins to the integration test) & write script to manually test all example sites w/ the util
  • use Gatsby colors for code block
  • add env feature flag & telemetry to measure usage in beta
  • support static queries
  • Wait for webpack

A sample error page

Screen Shot 2020-10-14 at 2 31 41 PM

@gatsbot gatsbot bot added the status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer label Oct 13, 2020
@KyleAMathews KyleAMathews marked this pull request as draft October 13, 2020 22:34
@KyleAMathews KyleAMathews added type: feature or enhancement Issue that is not a bug and requests the addition of a new feature or enhancement. and removed status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer labels Oct 14, 2020
Copy link
Contributor

@wardpeet wardpeet left a comment

Choose a reason for hiding this comment

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

I've added a few comments, I've tested this on my machine which is pretty strong (8 cores - 16 threads). I did not run with the flag on to see what impact this created on gatsby.

  • Kent c dodds site took 10s longer bundling dev bundle
  • gatsbyjs.com took me 20s longer

I would like us to get 0s slowdown before merging and I would like to put everything inside the process.env check.

packages/create-gatsby/package.json Outdated Show resolved Hide resolved

const webpackActivity = report.activityTimer(`Building development bundle`, {
id: `webpack-develop`,
})
webpackActivity.start()

initDevWorkerPool()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
initDevWorkerPool()
if (process.env.GATSBY_EXPERIMENTAL_DEV_SSR) {
initDevWorkerPool()
}

@@ -116,6 +83,8 @@ export async function startServer(
{ parentSpan: webpackActivity.span }
)

await buildRenderer(program, Stage.DevelopHTML)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
await buildRenderer(program, Stage.DevelopHTML)
if (process.env.GATSBY_EXPERIMENTAL_DEV_SSR) {
await buildRenderer(program, Stage.DevelopHTML)
}

Comment on lines 18 to 71
let oldHash = ``
let newHash = ``
const runWebpack = (
compilerConfig,
stage: Stage,
directory
): Bluebird<webpack.Stats> =>
new Bluebird((resolve, reject) => {
webpack(compilerConfig).run((err, stats) => {
if (err) {
reject(err)
} else {
resolve(stats)
}
})
if (stage === `build-html`) {
webpack(compilerConfig).run((err, stats) => {
if (err) {
return reject(err)
} else {
return resolve(stats)
}
})
} else if (stage === `develop-html`) {
webpack(compilerConfig).watch(
{
ignored: /node_modules/,
},
(err, stats) => {
if (err) {
return reject(err)
} else {
newHash = stats.hash || ``

// Make sure we use the latest version during development
if (oldHash !== `` && newHash !== oldHash) {
restartWorker(`${directory}/public/render-page.js`)
}

oldHash = newHash

return resolve(stats)
}
}
)
}
})

const doBuildRenderer = async (
{ directory }: IProgram,
webpackConfig: webpack.Configuration
webpackConfig: webpack.Configuration,
stage: Stage
): Promise<string> => {
const stats = await runWebpack(webpackConfig)
const stats = await runWebpack(webpackConfig, stage, directory)
if (stats.hasErrors()) {
reporter.panic(
structureWebpackErrors(`build-html`, stats.compilation.errors)
)
reporter.panic(structureWebpackErrors(stage, stats.compilation.errors))
}

// render-page.js is hard coded in webpack.config
return `${directory}/public/render-page.js`
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Diffs don't work well but I would go this route as it makes it a little bit more verbose. You'll faster see what the difference is between stages

let oldHash = ``
let newHash = ``
const runWebpack = (
  compilerConfig
): Promise<webpack.Stats> =>
  new Promise((resolve, reject) => {
    webpack(compilerConfig).run((err, stats) => {
      if (err) {
        return reject(err)
      } else {
        return resolve(stats)
      }
    })
  })


const watchWebpack = (
  compilerConfig,
  directory
): Promise<webpack.Stats> =>
  new Promise((resolve, reject) => {
    webpack(compilerConfig).watch(
      {
        ignored: /node_modules/,
      },
      (err, stats) => {
        if (err) {
          return reject(err)
        } else {
          newHash = stats.hash || ``


          // Make sure we use the latest version during development
          if (oldHash !== `` && newHash !== oldHash) {
            restartWorker(`${directory}/public/render-page.js`)
          }


          oldHash = newHash


          return resolve(stats)
        }
      }
    )
  }


const doBuildRenderer = async (
  { directory }: IProgram,
  webpackConfig: webpack.Configuration,
  stage: Stage
): Promise<string> => {
  let stats
  if (stage === 'develop-html') {
    stats = await watchWebpack(webpackConfig, directory)
  } else {
    stats = await runWebpack(webpackConfig)
  }

  if (stats.hasErrors()) {
    reporter.panic(structureWebpackErrors(stage, stats.compilation.errors))
  }


  // render-page.js is hard coded in webpack.config
  return `${directory}/public/render-page.js`
}

packages/gatsby/src/utils/dev-ssr/develop-html-route.ts Outdated Show resolved Hide resolved
Comment on lines 310 to 324
try {
const renderResponse = await renderDevHTML({
path: genericHtmlPath,
// Let renderDevHTML figure it out.
page: undefined,
store,
htmlComponentRendererPath: `${program.directory}/public/render-page.js`,
directory: program.directory,
})
const status = process.env.GATSBY_EXPERIMENTAL_DEV_SSR ? 404 : 200
res.status(status).send(renderResponse)
} catch (e) {
report.error(e)
res.send(e).status(500)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we keep the old code to make sure we're not changing behavior?

packages/gatsby/src/utils/start-server.ts Show resolved Hide resolved
import { buildHTML } from "../commands/build-html"
import { withBasePath } from "../utils/path"
import { buildRenderer } from "../commands/build-html"
import { renderDevHTML, initDevWorkerPool } from "./dev-ssr/render-dev-html"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we do require() inside the if statements below so we only load them when necessary, to keep side-effects at bay

@KyleAMathews
Copy link
Contributor Author

😆 I spent an hour trying to figure out why the time to bundle got longer with this PR and... it turned out it didn't — on master we just don't include the time to create the dev ssr bundle in the "Building development bundle" activity.

Mystery solved!

@KyleAMathews
Copy link
Contributor Author

I'm leaving the change to the develop activity as a) it's more accurate and b) currently there's no output while the dev-ssr bundle is being created so there's an odd long-ish pause during that time.

wardpeet
wardpeet previously approved these changes Nov 17, 2020
Copy link
Contributor

@wardpeet wardpeet left a comment

Choose a reason for hiding this comment

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

Awesome it works! There is some flakiness with hmr but it's behind a flag so 👍

@KyleAMathews
Copy link
Contributor Author

Could you post details on the HMR flakiness to the umbrella issue? #28138 Will handle it soon

Copy link
Contributor

@pieh pieh left a comment

Choose a reason for hiding this comment

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

Re-approve after master mege

@simonjoom
Copy link

simonjoom commented Dec 5, 2020

Hi, I did try & SSR in dev is working for me good. but what I constated:

const bodyStr = isClientOnlyPage ? `` : generateBodyHTML()

This test isClientOnlyPage do not generate the generateBodyHTML of the page.
Does that is a normal behaviour?

For me my index is a clientonlypage as I use alias on it..
if use in Gatsby-node :
matchPath: /:params, for node.path==="/"

unfortunately for index the lack of ssr give me not a good result as I generate body css class in ssr
I use my own replaceBodyHTMLString and the body class are generated in SSR only (I don't want to use the client for that)

to generate my classes I use

export const onRenderBody = ({ setBodyAttributes, setHtmlAttributes, setHeadComponents }, pluginOptions) => {
  const helmet = Helmet.renderStatic()
  setHtmlAttributes(helmet.htmlAttributes.toComponent())
  setBodyAttributes(helmet.bodyAttributes.toComponent())
  setHtmlAttributes({
    className: 'no-cssanimations'
  });
  setBodyAttributes({
    className: 'fp-waiting home page-template-default page page-id-54944 page-child parent-pageid-83590 style-color-wayh-bg hormenu-position-left hmenu hmenu-center-double header-full-width main-center-align menu-mobile-animated menu-mobile-transparent menu-custom-padding menu-mobile-centered menu-has-cta mobile-parallax-not-allowed ilb-no-bounce wpb-js-composer js-comp-ver-6.0.3 vc_responsive open-overlay-menu'
  })
...
}

I m not using helmet in client code (just for optimisation purpose like no helmet in client reduce the client code result )

So the question I have:
What is the reasons to not generate the Body html code for one clientonlypage in SSR ?
nota the function generateBodyHTML allow the code to render Attributes as well (and not only the get the body 'body'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: DX Developer Experience (e.g. Fast Refresh, i18n, SSR, page creation, starters) type: feature or enhancement Issue that is not a bug and requests the addition of a new feature or enhancement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants