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

Nuxt does not clean up CSS after navigation (and injects CSS pre-navigation) #22817

Open
matthew-dean opened this issue Aug 25, 2023 · 18 comments

Comments

@matthew-dean
Copy link

matthew-dean commented Aug 25, 2023

Environment

  • Operating System: Darwin
  • Node Version: v16.19.0
  • Nuxt Version: 3.6.5
  • Nitro Version: 2.5.2
  • Package Manager: pnpm@8.3.1
  • Builder: vite
  • User Config: extends, srcDir, build, hooks, runtimeConfig
  • Runtime Modules: -
  • Build Modules: -

Reproduction

https://stackblitz.com/edit/nuxt-starter-l1dvnf?file=app.vue

Describe the bug

There's actually two manifestations of this bug.

First bug:

  • Given two pages, A & B, and two layouts, X and Y
  • Page A uses Template X
  • Page B uses Template Y
  • When navigating to page A, it will mount the CSS for Template X
  • When navigating to page B, it will mount the CSS for Template Y
  • However, when navigating back to page A, it will not remove the CSS for template Y.
  • Bug: This means, effectively, that page A's rendering is unstable, and will render differently depending on preceding navigation.

Second bug:

  • Given the above, if you use pre-fetching with NuxtLink (<NuxtLink> without noPrefetch), then the CSS for template Y will be injected into the page when navigating to page A, even though it has no association with Template Y.
  • In other words, Nuxt eagerly injects CSS un-associated with the current page

Note that the first bug occurs whether or not you use noPrefetch.

Additional context

I originally thought this bug was associated with / the same bug as #3877, but was told that was not the case, so filing this new issue. I also thought it might be the exact same as this issue, but that issue is closed?

Logs

No response

@stackblitz
Copy link

stackblitz bot commented Aug 25, 2023

Fix this issue in StackBlitz Codeflow Start a new pull request in StackBlitz Codeflow.

@matthew-dean
Copy link
Author

@danielroe If there's a temporary workaround for this bug, I'd love to know! Thanks!

@matthew-dean
Copy link
Author

@danielroe Or anyone? Any advice on how to work around this?

Copy link
Member

apologies for the delay in looking at this.

@matthew-dean
Copy link
Author

@danielroe It's alright, we're all busy people. What I did in the short term was, because I was using Less / SCSS, I wrapped the entirety of styles with a class (which, of course, isn't ideal, since it will expand the length of every selector), and then set the HTML class per template. It works, but yeah I'd love to just have CSS styles work for templates.

@galaxyblur
Copy link

I'm seeing this issue when not using layouts -- simply two different pages. When I @import within the <style></style> block of a page, Page A will keep the css files loaded from Page B after navigating B->A.

@matthew-dean
Copy link
Author

@galaxyblur Oh okay, so it's safer to say that Nuxt 3 doesn't have proper CSS support yet?

@manniL
Copy link
Member

manniL commented Dec 10, 2023

@matthew-dean No, absolutely not! 😋

Page A will keep the css files loaded from Page B after navigating B->A.

IMO This is expected and you can use <style scoped> (Scoped Styles) to avoid this. No styles will be "unloaded" when navigating. If your page has global styles that alter the whole application (non-scoped ones), they will stay throughout the user journey after being loaded unless somehow handled in userland ☺️

The same applies to the issue mentioned above I'd say.

Fixed example using :deep + scoped styles.


Happy for some other opinions here though (cc @danielroe)

@danielroe
Copy link
Member

I agree with @manniL's take. You should not rely on whether a stylesheet is or isn't loaded. Nuxt may preload a stylesheet when the browser is idle, for example, to ensure that a page loads quickly and doesn't rely on additional network requests.

Vite may also bundle multiple pages' or layouts' CSS into a single file if they are very small.

These are implementation details.

Instead you should use scoped CSS, CSS modules, or unique selectors to ensure there are no conflicts. This is a matter of (IMO) best practice anyway.

@danielroe danielroe closed this as not planned Won't fix, can't repro, duplicate, stale Dec 11, 2023
@matthew-dean
Copy link
Author

I respectfully disagree, in that yes, it's understandable to preload assets, but

  1. it's completely counter-intuitive to actually mount assets from components that are not themselves mounted, and mounting / preloading should not be conflated concepts
  2. Simply using scoped CSS or CSS modules makes unfounded assumptions that Nuxt is being only used on sites that have been built entirely from scratch, and IMO worsens adoption for people migrating TO Nuxt.
  3. Leaving a trail of unused CSS behind you, or mounting CSS that is not applied is not benign, and will have a progressively worsening impact on browser performance. To be fair, it's hard to know how large that impact is in modern browsers, but style recalculation is linear based on the number of selectors + amount of mounted markup.

Can we not enable an option to not pre-mount CSS? I tried creating a custom Nuxt Link, but there was no way to use noPrefetch in those options. For some reason, that option isn't exposed. I tried figuring out some kind of hook to manually remove the stylesheet from the DOM when a particular layout is unmounted, but I couldn't figure out a way to find an identifier that would allow me to query / remove CSS associated with a particular layout or component.

I understand the need to say no for edge cases, so I'm not saying you need to add this as a feature, but could you at least enable a way for me to do it? I feel like even those avenues are blocked.

@danielroe
Copy link
Member

danielroe commented Dec 20, 2023

The behaviour here is not Nuxt's but Vite's; I would create a proposal or feature request there, that we can then benefit from in Nuxt.

@wavedeck
Copy link

wavedeck commented Jan 27, 2024

Page A will keep the css files loaded from Page B after navigating B->A.

IMO This is expected and you can use <style scoped>

While this is true for styles applied to individual pages, you cannot scope styles inside a layout, as this will not apply them to the pages using the layout.. so the only real workaround is to encapsulate the layout styles in another parent selector
(e.g. ".layout-a h1" and ".layout-b h1") or using completely unique selectors.

this issue drove me insane when i was trying to reduce css bundle size by splitting them into different layouts. The second you use NuxtLink without disabling prefetching, the styles will get combined again and can cause unexpected behaviour.

It also didn't help that this issue was closed, since it makes it more effort to find.

In general, I'm distressed that almost all of the major JavaScript SSR frameworks out there happen to have so many gotcha's when handling css. It's very hard to keep control and regularly lures me back to traditional SSR, where you have full control of the HTML for each page.

@manniL
Copy link
Member

manniL commented Jan 27, 2024

While this is true for styles applied to individual pages, you cannot scope styles inside a layout, as this will not apply them to the pages using the layout..

You can use the deep pseudoclass for that :)

@slps970093
Copy link

The behaviour here is not Nuxt's but Vite's; I would create a proposal or feature request there, that we can then benefit from in Nuxt.

Is there a link to this question? I wonder if it's already here

@matthew-dean
Copy link
Author

matthew-dean commented Mar 21, 2024

Note: I discovered today that this is also a problem for just page-to-page CSS, not just layout CSS. Navigating to a new page does not un-mount the CSS for a previous page. Therefore, if a page alters any CSS globals for that page, while the page is mounted, then those globals will persist long after the page is gone. I'm not sure why Nuxt would want the memory leak / performance issue of just endlessly stacking CSS while you navigate around. If you never refresh your page and navigate through 1,000 pages, won't you end up with 1,000 style tags trailing behind you? 🤔

@manniL

No styles will be "unloaded" when navigating

Style tags are not benign if "un-used". The browser still has to build a style tree, and more styles means that takes longer, and it still has to traverse the DOM to know what tags have styles applied, which includes tags in the head. The DOM can (and will) degrade in performance the more elements are added. Leaving style trash in the wake of navigation just doesn't make a lot of sense.

@matthew-dean matthew-dean changed the title Nuxt does not manage layout CSS correctly Nuxt does not clean up CSS after navigation (and injects CSS pre-navigation) Mar 21, 2024
@matthew-dean
Copy link
Author

I wrote a simple test with a Nuxt plugin, which looks like this:

import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin({
  name: 'css-register',
  hooks: {
    'page:loading:start'() {
      const styles = document.querySelectorAll('style')
      console.log('Number of styles:', styles.length)
    }
  }
})

I loaded a /links route.

Number of styles: 13

I navigated to a /groups route

Number of styles: 21

I navigated back to /links

Number of styles: 22

I navigated back to /groups

Number of styles: 23

Back to /links

Number of styles 23 (okay, interesting)

Back to /groups

Number of styles: 24

🙃

@danielroe
Copy link
Member

Reopened to investigate.

@metkm
Copy link

metkm commented Apr 21, 2024

Reopened to investigate.

@danielroe I think something like this would be useful when using view-transitions. When you need to animate something with different aspect ratios you should apply different object-fit property depending on the direction. https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/#handling-shape-and-size-changes

For example, when you go from a wider image to a smaller image

::view-transition-old(cover) {
  object-fit: cover;
}

::view-transition-new(cover) {
  object-fit: contain;
}

vice versa

::view-transition-old(cover) {
  object-fit: contain;
}

::view-transition-new(cover) {
  object-fit: cover;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants