Sass and CSS Import Performance in Angular
In this short post, I want to cover an everyday performance issue I see occur in Angular applications that use Sass or CSS imports. The Angular CLI supports Sass, CSS and Less to style global application styles as well as component styles. Angular components styles have a useful CSS encapsulation mechanism that ensures any component CSS is local to the component and does not globally alter any styles. Let's take a look at a simple component with some CSS styles.
<!-- duplicate-styles.component.html -->
<button>duplicate styles</button>
/* duplicate-styles.component.css */
@import './../../styles/config';
@import './../../styles/buttons';
button {
@extend .btn;
background-color: blue;
}
In this component, we have a button that needs to be explicitly styled for a feature that diverges from the standard buttons in our application. We want this button to have the standard styles of a button but also adjust the color for our specific use case. To do this, I am importing the buttons Sass file. The _buttons.scss
file contains all the global button styles for my Angular application. In the component sass file, I am extending the global .btn
class. So everything works! My component extends the .btn
class and adds its specific feature color. Feature done right? Well not quite.
We created a performance issue with the bundle size of our application. Let's take a look at that _buttons.scss
file and see what is in there.
/* _buttons.scss */
@import 'config';
.btn {
background-color: $color-theme;
padding: 12px;
color: #fff;
}
.btn.warning {
background-color: $color-warning;
}
.btn.error {
background-color: $color-error;
}
Ok, pretty standard styles in _buttons.scss
so whats the issue? Well, it comes back to our specific feature component importing the _buttons.scss
. Let's take a look at the compiled JavaScript from the component in the browser.
// duplicate-styles.component.ts compiled JIT output
(function(
jit_createRendererType2_0,
jit_viewDef_1,
jit_elementDef_2,
jit_textDef_3 /*``*/
) {
// All the CSS from _buttons.scss has been copied into the bundle
var styles_DuplicateStylesComponent = [
`
.btn[_ngcontent-%COMP%], button[_ngcontent-%COMP%]{background-color:#4CAF50;padding:12px;color:#fff}
.btn.warning[_ngcontent-%COMP%], button.warning[_ngcontent-%COMP%]{background-color:orange}
.btn.error[_ngcontent-%COMP%], button.error[_ngcontent-%COMP%]{background-color:red}
.btn.feature[_ngcontent-%COMP%], button.feature[_ngcontent-%COMP%]{background-color:purple}
button[_ngcontent-%COMP%]{background-color:blue}
`
];
var RenderType_DuplicateStylesComponent = jit_createRendererType2_0({
encapsulation: 0,
styles: styles_DuplicateStylesComponent,
data: {}
});
function View_DuplicateStylesComponent_0(_l) {
return jit_viewDef_1(
0,
[
(_l()(),
jit_elementDef_2(
0,
0,
null,
null,
1,
'button',
[],
null,
null,
null,
null,
null
)),
(_l()(), jit_textDef_3(-1, null, ['duplicate styles']))
],
null,
null
);
}
return {
RenderType_DuplicateStylesComponent: RenderType_DuplicateStylesComponent,
View_DuplicateStylesComponent_0: View_DuplicateStylesComponent_0
};
});
Notice in the compiled JS we see all the styles from the button.scss
file embedded into the compiled JavaScript for our feature component. Angular's CSS encapsulation mechanism while powerful does not tree shake any of styles. Without tree shaking this means any unused style from an imported Sass or CSS files is bundled into that component. We have two copies of our site-wide button styles.
It can be really easy to accidentally import a large stylesheet multiple times in an Angular application. This duplication leads to unnecessary bloat in our bundles. How do we fix this issue?
High Specificity Global Styles
We have a few options to correct our duplicated styles. The first is to create a specific global style.
/* _buttons.scss */
@import 'config';
.btn {
background-color: $color-theme;
padding: 12px;
color: #fff;
}
.btn.warning {
background-color: $color-warning;
}
.btn.error {
background-color: $color-error;
}
.btn.feature {
background-color: purple;
}
Now in our template of our feature component we have the following:
<!-- global-styles.component.html -->
<button class="btn feature">global css class</button>
Now we can remove the custom component level styles all together. Now notice this isn't ideal as we are losing the benefit of local component styles. We could instead go another route and take advantage of some more advanced Sass features.
Sass Mixins
Another option is to leverage Sass mixins to create a button generator of sorts to create buttons as needed. A Sass mixin is a function that can generate CSS for us. Let's look at the _mixins.scss
file.
// _mixins.scss
@mixin btn-generator($btn-color) {
// take a color as a parameter
background-color: $btn-color;
padding: 12px;
color: #fff;
}
Our btn-generator
mixin will allow us to stamp our easily create buttons in our project now. Now we can remove the global specific style and use the scoped component.
// mixin-styles.component.scss
@import './../../styles/config';
@import './../../styles/_mixins';
button {
@include btn-generator(red);
}
Notice how we no longer import the _buttons.scss
file? Now our component will not be duplicating the global buttons style sheet into its bundle. We use the btn-generator
mixin to create an instance of button styles that will only apply to our feature component styles. Our compiled component now only bundles the CSS the mixin generated for us.
// mixin-styles.component.ts compiled JIT output
(function(
jit_createRendererType2_0,
jit_viewDef_1,
jit_elementDef_2,
jit_textDef_3
/*``*/
) {
// Only the single generate button style
var styles_MixinStylesComponent = [
`
button[_ngcontent-%COMP%]{background-color:red;padding:12px;color:#fff}
`
];
var RenderType_MixinStylesComponent = jit_createRendererType2_0({
encapsulation: 0,
styles: styles_MixinStylesComponent,
data: {}
});
function View_MixinStylesComponent_0(_l) {
return jit_viewDef_1(
0,
[
(_l()(),
jit_elementDef_2(
0,
0,
null,
null,
1,
'button',
[],
null,
null,
null,
null,
null
)),
(_l()(), jit_textDef_3(-1, null, ['mixin styles']))
],
null,
null
);
}
return {
RenderType_MixinStylesComponent: RenderType_MixinStylesComponent,
View_MixinStylesComponent_0: View_MixinStylesComponent_0
};
});
Another best practice you may have noticed was we store sass variables in a _config.scss
. The _config.scss
allows us to share variables across sass files, but we must ensure there is no other CSS or we will get the same duplication issue. This same rule would apply if you used custom CSS properties.
As you can see Angular's style CSS encapsulation mechanism is useful but be aware of how you use style imports with component level styles. Check out the working demo in the link below!