styled-components: Dynamic style change causes custom fonts to be re-requested from the server

I couldn’t tell if this has anything to do with #1576, but it seems like potentially its own issue. I don’t have @media rules, but I do have @font-face rules.

System:

  • OS: macOS High Sierra 10.13.3
  • CPU: x64 Intel® Core™ i5-4278U CPU @ 2.60GHz
  • Free Memory: 220.39 MB
  • Total Memory: 8.00 GB
  • Shell: /bin/bash - 3.2.57

Binaries:

  • Node: 6.10.3
  • Yarn: 1.5.1
  • npm: 5.7.1
  • Watchman: 4.9.0

npmPackages:

styled-components:

  • wanted: ^3.2.1
  • installed: 3.2.1

Reproduction

I have a style that includes:

  background: ${props => (props.checked ? '#000' : 'transparent')};

and my global styles look like:

injectGlobal`
  @font-face {
    /* Normal font-face declaration... */
  }
  body {
    font-face: 'Sofia Pro', sans-serif;
  }
`

When the checked prop changes, it causes the fonts in question to be re-requested unnecessarily. I assume something about the selector dynamically changing is causing the browser to touch/replace the injectGlobal rules for some reason.

I confirmed this doesn’t happen with styled-components 3.1.x.

(Nothing about the dynamic style references fonts or anything like that, so it’s not that a previously-unused :focus or :active or :checked rule is suddently applying.)

Here’s a GIF:

fonts

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 55
  • Comments: 60 (16 by maintainers)

Commits related to this issue

Most upvoted comments

It does matter since the browser will discard the fonts for a split second resulting in a font flicker. Depending on the amount of “styled lines / components” the flicker takes longer. In a very bad case I noticed the flicker even messes with the position of some elements since their height/width are dependent on the font it’s size.

This issue is still relevant, even with v4 createGlobalStyle.

Seems like this is wrongly closed, im still seeing this issue in V4. @probablyup your example is using a url import, which seems to fix the problem, however if your font is local the problem, at least - for me persists.

While annoying, does this end up mattering? Since the fonts are cached by the browser after the first request, I’m not sure this matters.

v4, still, fonts are re-requested inside createGlobalStyle e.g. when routes change

Maybe, we should make something like createStaticStyle for things that never change like global fonts? I know that pure CSS is static enough, but if you use webpack you have to install style-loader for a couple of fonts

Going to close this as the solution has been identified several times in this thread. Set your caching headers appropriately.

@kpoelhekke im also seeing this in dev mode.

I should note that the reason I originally noticed this wasn’t the network request directly, but rather the FOIT caused by the font reloading, which caused everything to disappear for a second. This made me think I had a bug/was doing something very wrong (like causing a component to unmount or something).

@uptown @jvarahuayta When using dynamic props, the related CSS is duplicated to represent the new variation, this creates a new CSS class and is appended to the DOM. The browser will notice that and request the URL.

If the URL has not changed, the only reason for a request to go through and download again is if you have not set the appropriate cache headers, this is common when developing as you want immediate updates to content that changes, if you don’t want that, you can configure your server(like webpack-devserver) to set cache headers for your requests responses, but if your content is cached for a long time, you might think you have a bug when the same request does not return the new version of your content, you’ll need to invalidate/clear the cache as a result. You’ll still see a request made when you have cache headers, but the request is cached, so no extra bandwidth by downloading again(good for production deploy).

The snippet you have shared, has moved the URL to an inline style directly on the element. So as long as it does not change, no new request will be made. The remaining CSS will create a class and append a new style tag in the head. If you have other dynamic props there, they will still do as described and duplicate the CSS, just not the inline styles.

You should still experience the “problem” if the URL changes, and then changes back to what it was before, or anything else that’d reapply the URL. The actual issue you’re likely experiencing is as mentioned above, you don’t have cache headers during development setup, it’s not going to be a problem in production.

I’m running into this issue and something like createStaticStyle would be really useful for me

changing component state also re-rendering the font which is causing flicker in screen.

I can confirm that I still also see this exact issue in v4 after updating from v3, and font-display: fallback; does nothing for me. The workaround I’m using for now is putting the @font-face rules outside of styled-components entirely.

Can confirm with v3.3.3 on OSX Chrome, Safari, Safari Technology Preview, not Firefox or Firefox Nightly.

My fix was to append a new stylesheet containing the font-face rules (this was easy as my font-face rules were already modularized and importable as a string) into the header while removing the rules from my injectGlobal. This solution works for me because my application does not make use of externally-dependent (.css, .scss, .sass) stylesheets.

import { FONTFACE_CIRCULAR } from 'app/styles/fonts';
const loadFonts = () => {
  const head = document.getElementsByTagName('head')[0];
  const style = document.createElement('style');
  style.innerHTML = FONTFACE_CIRCULAR;
  head.appendChild(style);
};

ReactDOM.render(<App />, document.getElementById('app'), loadFonts);

@probablyup I checked. Having actually cacheable headers (max-age>0) resolves this issue 💃

Is this problem only related to fonts? If yes then I guess it’s not so important as it’s rather natural to cache them (because why would you like to refresh them often anyway?) But if it also affects other css loading mechanisms (background-image: url()?) then this might be serious 😄

@MrOpperman the best solution right now is to just use at-import or a style/link tag separate from styled-components unfortunately

@polarathene

First of all, our server already has cache header for static files and the files are supplied with CDN. So in our real env, we don’t suffer with refreshing images even though the browser attempts getting the images from its hard disk or memory (or 304 response)

However, the cause of this problem is styled-component continuously appends new rules in the same <style> tag that already contains a lot of css defines of other components. It means the browser may calculate style tag’s contents again and again.

Therefore, I suggest that appending new rules to new style tags OR giving a new style tags for each component with dynamic rules.

You can say we can bypass the problem with setting cache header, but still the cause is not solved and there is a possibility that this problem causes other problem in the future.

For developers who are suffering from this, this snippet solved my problem.

export const SomeComponent= styled.div.attrs({
  style: {
    backgroundImage: `url(${imageUrl})`,
  },
})`
// css
`;

@uptown that’s unfortunately not the whole picture of the problem. CSSOM is inherently a little quirky but it is fast because the browser doesn’t reparse all CSS in the tag. Now, this doesn’t apply to development since there we fall back to using TextNodes. But since we regard this as a fallback, this is acceptable because the performance and experience is already degraded.

Now, with insertRule there’s a specific quirk where inserting any rule can retrigger some assets to be reloaded even when an entirely unrelated rule hasn’t changed.

The problem is that this can apply to all assets. So even if we work around this for font-face and import at-rules, which should maybe even be handled in actual CSS files in the first place, there are still other assets that might be affected.

So in summary, this looks like a bigger issue than it is in development, and in production it only happens rarely, especially with cache headers and requires an unreasonable amount of complexity to be fixed for all edge cases.

Edit: Reading through some comments here and some of this is also related to the use of GlobalStyle. It’s worth saying that GlobalStyle is an escape hatch and that keeping global styles outside of styled-components or CSS-in-JS is often desirable, especially for caching in general.

The reality is when the GlobalStyles component gets rerendered, the browser reparses the sheet and if the items being accessed in the styles rely on external files the caching headers are checked and redownloaded if outside the range. It’s standard for how browsers work, just in typical CSS you aren’t ever updating the styles since they’re hardcoded.

@sarneeh This issue is affecting background-image: url() for me, weirdly only in Safari though…

has anyone checked to make sure the fonts are served with appropriate caching headers?

Try using font-display: fallback; in the font face rule. I’ve found it helps.

On Sun, Oct 21, 2018, 1:58 PM Sami notifications@github.com wrote:

Seems like this is wrongly closed, im still seeing this issue in V4. @probablyup https://github.com/probablyup your example is using a url import, which seems to fix the problem, however if your font is local the problem, at least - for me persists.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/styled-components/styled-components/issues/1593#issuecomment-431694526, or mute the thread https://github.com/notifications/unsubscribe-auth/AAiy1hKPTXs85cDDMaHMEqCxL_fimRJ4ks5unMPqgaJpZM4SlIaO .

Im having the same issue. Im using nextjs with styled components and I have set background-image and font in body using <GlobalStyle /> and background-image flashes with original color then turns back to white and font doesn’t show up at all.

This is all in production env. When in dev env everything works as it should.

Have the same issue in background-image: url() mechanism in route change(nested routes A,B with same parent layout, this layout have the background). But the background-image second request is only in the first navigation from A->B or viceverse; in the second navigation the re-request is not made.

@joebentaylor It’s nothing to do with Gatsby. You may experience in during gatsby develop due to the dev server having cache headers having cache disabled. If you want to change that you need to change the config of the webpack dev server being used.

If you do gatsby build and then gatsby serve it will serve production version which does have cache enabled. Otherwise serve the site via any webserver, such as Netlify, nginx, etc.

Here’s a CodeSandbox demo 👀

Yes @richardhsueh this is a work around

Hmm maybe the external fonts are getting something etag or cache control set so the browser knows it doesn’t need to do anything. On Tue, Nov 13, 2018 at 2:41 AM Sami notifications@github.com wrote:

@kpoelhekke https://github.com/kpoelhekke im also seeing this in dev mode.

— You are receiving this because you modified the open/close state.

Reply to this email directly, view it on GitHub https://github.com/styled-components/styled-components/issues/1593#issuecomment-438182234, or mute the thread https://github.com/notifications/unsubscribe-auth/AAiy1t2pp3Pw2YqiRFkjaMKGbYscsCBiks5uuoWggaJpZM4SlIaO .

Here (v4.0.3) the problem only seems to happen for fonts which are in a local folder. Fonts loaded from external resources do not load again. Furthermore it doesn’t happen in dev mode, only on production mode.

Yes, it is still an issue in v4. font-display: fallback does not work. See also #400. /cc @probablyup

I don’t see this happening with the v4 beta: https://codesandbox.io/s/kk1y6k54jr

Going to close. If someone can come up with a v4 reproduction feel free to start a new ticket.

In my case, i am not using @import but met same issue. while I’ve set my @font-face rules with injectGlobal, some non-global styles used from components are combined into same <style data-styled-components> tags.

So when i changed first time on some props like <OtherStyledComponent show={true}> some conditional styles are loaded into <style> tag, and re-calculate whole styles in that tag, including @font-face.