styled-components: Server Side Rendering: First paint missing some styles (FOUC)

Environment

System:

  • OS: macOS High Sierra 10.13.5
  • CPU: x64 Intel® Core™ i5-4308U CPU @ 2.80GHz
  • Memory: 82.71 MB / 8.00 GB
  • Shell: 3.2.57 - /bin/bash

Binaries:

  • Node: 8.9.4 - ~/.nvm/versions/node/v8.9.4/bin/node
  • npm: 6.1.0 - ~/temp/Martin/metrics-dashboard/node_modules/.bin/npm
  • Watchman: 4.7.0 - /usr/local/bin/watchman

npmPackages:

  • babel-plugin-styled-components: ^1.5.1 => 1.5.1
  • styled-components: ^3.2.6 => 3.2.6

Reproduction

The chunk of server side rendering code used:

        const sheet = new ServerStyleSheet()
        let html = ReactDOMServer.renderToString(sheet.collectStyles(
          <Provider store={store}>
            {<RouterContext {...renderProps} />}
          </Provider>
        ))
        const styleTags = sheet.getStyleTags() // or sheet.getStyleElement()

My index template (index.ejs) as such:

<!DOCTYPE html>
<html>

<head>
  <%- styleTags %>
</head>
<body style="margin: 0; font-family: 'PT Sans', Arial;">
  <div id="root" style="height: 100%;">
    <%- html %>
  </div>
  <script type="text/javascript" charset="utf-8">
    window.__REDUX_STATE__ = '<%= reduxState %>';
  </script>

</body>
</html>

And just in case some styled-component code, though I don’t expect this to be the issue (possibly because of using extend?).

//   H1.js file

import styled from 'styled-components';

const H1 = styled.h1.attrs({ className: 'h1' })`
    font-size: 4rem;
`;

export default H1;

//   end of H1.js file

//   WidgetTitle.js file
import NormalH1 from "components/H1";

const WidgetTitle = NormalH1.extend.attrs({ className: 'widget__title' })``;

export default WidgetTitle;

//   end of WidgetTitle.js file

Steps to reproduce

  1. Server side rendering set up as shown above, following closely the steps here: https://www.styled-components.com/docs/advanced#server-side-rendering.

Expected Behavior

For all styles to be applied during first paint (like so below).

First paint expected

Actual Behavior

Only some styles being applied on first paint, while some rules (specifically font-size) seems to be applied only after the JS bundle is parsed and executed.

first paint missing font size rules

Remarks

As you can see in the two screenshots, I get a flash of content where the font-size rule isn’t respected. I had this same issue with font-family actually, but dealt with it by putting it as an inline style on my <body> tag. Possibly this is a Flash of Unstyled Text (FOUT), but I’m honestly quite lost as to why these specific rules are being ignored on the first paint.

I state that the rules seem to be applied only after the main JS bundle is loaded/parsed/executed because of examining the request for app.js closely in the Chrome Network panel. Is this possible a webpack issue where I need to optimize the bundle further?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 7
  • Comments: 66 (32 by maintainers)

Most upvoted comments

@probablyup - moving the <StyleSheetManager> to a separate block after getDataFromTree yielded the same effect, strangely.

The only fix I can think of, whilst also rendering out a separate <Html> React component and passing sheet.getStyleElement() to it, is to run renderToString on both parts, i.e.:

// ...

// Create a new styled-components instance
const sheet = new ServerStyleSheet();

const components = (
  <StyleSheetManager sheet={sheet.instance}>
    <ThemeProvider theme={defaultTheme}>
      <ApolloProvider client={client}>
        <StaticRouter location={ctx.request.url} context={routerContext}>
          <Root />
        </StaticRouter>
      </ApolloProvider>
    </ThemeProvider>
  </StyleSheetManager>
);

// Render the Apollo tree
await getDataFromTree(components);

// ...

// Create response HTML
const html = ReactDOMServer.renderToString(components);

// Pass in the rendered props to a final React wrapper
const reactRender = ReactDOMServer.renderToString(
  <Html
    css={output.client.main("css")!}
    helmet={Helmet.renderStatic()}
    html={html}
    js={output.client.main("js")!}
    styles={sheet.getStyleElement()}
    window={{
      __APOLLO_STATE__: client.extract(),
    }} />,
);

// Set the return type to `text/html`, and stream the response back to
// the client
ctx.type = "text/html";
ctx.body = `<!DOCTYPE html>${reactRender}`;

… which seems to work fine.

My main point is that this is a material change vs. SC v3. In the previous version, it wasn’t necessary to call renderToString twice. I could pass in sheet.getStyleElement() to the component that got rendered out and it worked fine.

Just curious whether this change was intentionally part of the new API? Maybe a performance thing?

@franky47 I think your issue is actually a next.js usage issue. Afaik the rendered elements in _document.js generate static markup, which might cause issues, since that’s not meant to be concurrency safe and it’ll also mess with our context-driven global styles.

If you instead include the global style component in _app.js it should work correctly: https://github.com/zeit/next.js/#custom-app

Yeah, I understand most of the team has mentally (and possibly literally) checked out of v3 and are focusing on moving forward with the better designed API in v4. With a project like React-Static, however, we don’t have that luxury when hundreds of our users sites use styled-components v3.

Good luck on v4!