Importing CSS only once

If you have two css files:

a.css

@import "./c.css";

b.css

@import "./c.css";

c.css’ rules are applied twice. Can we have an alternative syntax where the stylesheet is only applied if not already done so?

2 Likes

AFAIK current best advice is simply to avoid @import entirely due to performance problems: https://gtmetrix.com/avoid-css-import.html

A redesigned @import should probably address the performance issue too.

Note that interestingly HTML imports have de-duplication, so using those to bring in stylesheets is sort-of there.

@AshleyScirra The performance problem that article describes is fixed by <link rel="preload">. Is there another performance issue you are thinking of?

Interesting note about Imports, but does it make sense for a stylesheet to not be able to describe its own dependencies?

If such a case did exist in nature, it seems like the two would actually do something potentially different in terms of rendering because of the cascade, no? Is there a reason that one or more is better or worse besides perf? Just curious. I’m not entirely sure why it is designed that way as opposed to the other - have you checked the archives of www-style?

@briankardell You are comparing two things but I’m not sure what the two things you are comparing, so I can’t answer. Can you clarify?

Just mocking this up with these three files, what is the expected currentColor value in three.css? Doing an importOnce style system messes with the natural cascading order of CSS. This is just once simple example of where things go wonky and get confusing. I’m sure there are more examples that could be thought of with more complex selectors becoming problematic as well, which would make for a difficult time debugging things.

one.css ```css .something { color: red; } ```
two.css ```css @importOnce('one.css');

.something { color: blue; }

</details>

<details>
<summary>three.css</summary>
```css
@importOnce('two.css');
@importOnce('one.css');

.anotherThing:has(.something):after {
  color: currentColor;
}

I don’t find that confusing at all. Your example is equivalent to writing:

<link rel="stylesheet" href="one.css">
<link rel="stylesheet" href="two.css">
<link rel="stylesheet" href="three.css">

If you had used @import instead it would be equivalent of writing:

<link rel="stylesheet" href="one.css">
<link rel="stylesheet" href="two.css">
<link rel="stylesheet" href="one.css">
<link rel="stylesheet" href="three.css">

You can write a similar example of JS modules that clobber each other, but I don’t think that’s a good reason not to have modules.

@importOnce would be useful, for example, if you have a variables.css that contains :root defined variables that you want to use in other stylesheets.

I’m curious, what’s the underlying issue you’re trying to solve?

With @import, you’re presumably using preload and technologies like appcache or service workers can be used to mitigate the extra http call so it’s probably not performance related.

While the sheet may be imported twice, it doesn’t change the specificity of the rules, just the cascade (which @Garbee pointed out) which is something you’d want when @import-ing twice.

If it’s merely to help identify dependencies, why not use a preprocessor and skip native @import altogether?

I’m saying that the current behavior works as @Garbee illustrated and… yeah, it seems wonky… Same as including two links really though - should that change too? I’m not arguing one way or another, your thing sounds… kinda rational to me, but I presume there was probably discussion in making that set of choice tradeoffs (i.e. how it works now) as opposed to “if it already isn’t imported”/once thing you are suggesting which would effectively disallow import from creating that kind of situation. So i’m saying: have you looked to see if there are design conversations around this from way back already and read through them? I expect that recognition of/debate with original points as to why they are proving inadequate would be necessary to offer an alternative.

I really like this idea!
Also consider the following:

<link once rel="stylesheet" href="component.css">
<link once rel="stylesheet" href="component-extend.css">
<component>my component</component>

<!-- this will possibly overwrite rules in component-extend.css -->
<link once rel="stylesheet" href="component.css"> 
<component>my component again</component>

I dont want component-extend.css to overwrite some rules in component.css

And - since LESS 1.4.0, @import-once is removed and is now default behavior for @import!

1 Like

@import-once would be great! It makes sense for code organization and not duplicating the CSS code. Sure runtime could be slow loading all the files (just like with ES Modules), but tooling can compile/bundle it for production., and specialized HTTP/2 servers can also help without requiring build steps.

1 Like

The biggest pain point I have with CSS is that I can’t write shared re-usable CSS code in a robust way.

If I have two features, A.css and B.css, if they both need to import shared styling that they both with to extend or override, one would be tempted to write @import shared.css in both files.

This is bad because, if an end user imports A and B like so:

@import "path/to/A.css";
@import "path/to/B.css";

then it is effectively the same as if the CSS were written pasted into a single sheet in the following order:

shared
A
shared (duplicate)
B

This is problematic because if A.css intended to override values in shared.css, then the import of B.css will undo the override and set it back to the original value. Only any override in B.css will work, because it comes last.

What we’d want is a syntax like @import-once "shared.css", that would result in shared.css only ever being imported once, on the first occurrence of the import. If A.css and B.css both used @import-once "shared.css" (or some similar syntax), then the result would be similar to having pasted all the styling into a single sheet like this:

shared
A
B

This is a lot better, because A and B can override shared with less chance of undoing each other.

There still exists the possibility that A and B can override properties in the same class, but that would be due to bad design on the author’s end. A would want to specify overrides for a unique selector, different from the one in B. The main difference is that the author has the choice (using selectors) to decide whether or not B overrides anything in A in a sane way.

Without @import-once the author can not make the choice to override or not in a sane way without sacrificing developer ergonomics. The library author of A.css and B.css would have to not use @import "shared.css" inside of A.css and B.css, and would need to tell the end users to write the code in the following fashion that does not scale:

@import "shared.css"
@import "A.css"
@import "B.css"

In otherwords, the status quo, currently, is that a library author needs to tell end users to worry about what all the dependencies of a feature are, and to import those dependencies only once.

Imagine if A.css and B.css actually depended on 10 more files each. It would be huge pain for the end user of A.css and B.css to have to also write up to 20 other @import statements just to be able to use A.css and B.css.

Furthermore, the dependencies of A.css and B.css may be implementation details that the end user need not necessarily case about. If the dependencies need to be changed by the library author, the author needs to tell the end user to update the @import statements, which is very non-ideal.

With @import-once, the library author would write the dependencies of A.css and B.css, and does not need to tell the end user to update dependencies when they change.

Imagine if in JavaScript with ES Modules, the end user of a library was responsible for importing all the dependencies of the end user dependencies. That would be very burdensome and would not scale well, and such a feature would simply never land in a runtime. Thank goodness ES Module in reality allow dependencies to specify dependencies without modules be re-evaluated for every time they are imported in any file. That would be a very bad developer experience to have to live with.

This is what (native pure) CSS developers have to live with, and why they always have to reach out for build tools; writing re-usable robust shared CSS code is currently not possible, and taking advantage of @import means delegating all dependency resolution of import statements to the end user.

I opened a GitHub issue for this. Please share any thoughts there: