Angular-cli: Non-optimal production build of unused CSS in all components/directives

Created on 16 Feb 2018  Â·  14Comments  Â·  Source: angular/angular-cli

I created the component and added a CSS file to it, let's say our developers write a lot of suboptimal code and forget to clean it up:

image

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: [
    './app.component.css',
    './materialize.css'
  ]
})
export class AppComponent {
  title = 'app';
}
$ ng build --prod 
chunk {0} polyfills.f20484b2fa4642e0dca8.bundle.js (polyfills) 59.4 kB [initial] [rendered]
chunk {1} main.9414139e8f04fe910b03.bundle.js (main) 351 kB [initial] [rendered]
chunk {2} styles.9c0ad738f18adc3d19ed.bundle.css (styles) 79 bytes [initial] [rendered]
chunk {3} inline.7d41986913723cd10387.bundle.js (inline) 1.45 kB [entry] [rendered]

PS: main.js 351Кb? Why? I usage one component and I expect that the collector will do everything for me as it should (removed unused CSS code).

image

If I were using assets, I probably would not need to delete the unused code. And so it turns out, not only that I was generated a huge CSS, so for him also added attributes for emulating Shadow DOM.

Tested By unusedCSS extension

image

Why do not you set config up the .angular-cli.json for remove unused css?

You use webpack, but you do not use steep plugins (and loaders):
plugin: https://github.com/purifycss/purifycss (8k stars)
loader: https://github.com/webpack-contrib/purifycss-webpack

@IgorMinar Your team is chasing the decrease in the size of the bundle, but in the end, some novice developers may not be aware of such moments and do not believe that their applications may decrease in size.

Versions

$ ng -v
_                      _                 ____ _     ___
/ \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
/ â–³ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
/ ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
|___/

Angular CLI: 1.6.6
Node: 8.9.1
OS: win32 x64
Angular: 5.2.5
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

@angular/cli: 1.6.6
@angular-devkit/build-optimizer: 0.0.42
@angular-devkit/core: 0.0.29
@angular-devkit/schematics: 0.0.52
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.9.6
@schematics/angular: 0.1.17
typescript: 2.5.3
webpack: 3.10.0

Repro steps

https://github.com/splincode/css-unused-angular
https://stackblitz.com/github/splincode/css-unused-angular

devkibuild-angular feature

Most helpful comment

That's what I prototyped here http://codelyzer.com/rules/no-unused-css/.

All 14 comments

In Angular 6, many people want to see the possibility to collect applications with the flag for removing unused CSS, or at least warnings (unused css, js, ..) so that the compiler can output.

Its an interesting feature that could be added. The question is how easy would it be to integrate with angular and dynamic addition/removal of classes.

@splincode did you use purifycss on your example application and saw any results?

I started working with angular from the second version, but I did not use the CLI. I used a webpack-starter. At Angular 5 could only go with Angular CLI.

But in our projects I used the purification of CSS directly without loaders. I pointed out that he analyzed the entire src/app/*.html directory with other files (+ jquery plugins or ts directives).

But we needed to use the encapsulation parameters (ViewEncapsulation.None), because angular generated attributes (for emulated shadow dom)

@jotatoledo @IgorMinar We were able to destroy unused styles at the component level

import * as path from 'path';
import * as ngcWebpack from 'ngc-webpack';
import * as fs from 'fs';

const cssDependants = {};

function extractCssDependants(componentPath, source) {
  if (source.includes('@Component')) {
    const decorator = source.match(/@Component\(([\s\S]*)\)/)[1].split('\n').join('');
    const templateUrl = decorator.match(/templateUrl: '(.*?)'/)[1];
    const styleUrls = decorator.match(/styleUrls: \[(.*?)\]/)[1].split(',').map(x => x.trim().slice(1, -1));

    const templatePath = path.resolve(path.dirname(componentPath), templateUrl);
    const stylePaths = styleUrls.map(x => path.resolve(path.dirname(componentPath), x));

    stylePaths.forEach((v) => {
      if (!cssDependants[v]) { cssDependants[v] = []; }
      if (templateUrl) { cssDependants[v].push(templatePath); }
      cssDependants[v].push(componentPath);
    });
  }
  return source;
}
function purifyCSS(cssPath, source) {
  if (cssDependants[cssPath]) {
    const files = cssDependants[cssPath]
      .map(f => fs.readFileSync(f, 'utf8'))
      .reduce((a, b) => a + b);
    console.log(cssPath);
    return purify(files, beautify(source), {minify: true, info: true});
  }
  return source;
}

export function purifyIzolated(config: webpack.Configuration, options: WebpackConfigOptions<BuildOptions>) {
  if (options.buildOptions.environment === 'prod' && options.buildOptions.aot) {
    const root = path.resolve(options.projectRoot, options.appConfig.root);
    const tsConfigPath = path.resolve(root, options.appConfig.tsconfig);
    const mainPath = path.resolve(root, options.appConfig.main);

    const mayBeComponentRegexp = new RegExp(`^${root}.*ts$`);
    const ngc = new ngcWebpack.NgcWebpackPlugin({
      AOT: options.buildOptions.aot,
      tsConfigPath: tsConfigPath,
      mainPath: mainPath,
      readFileTransformer: {
        predicate: mayBeComponentRegexp,
        transform: extractCssDependants
      },
      resourceTransformer: purifyCSS
    });

    const compilerPluginIndex = config.plugins.findIndex(x => x.constructor.name === 'AngularCompilerPlugin');
    config.plugins[compilerPluginIndex] = ngc;
  }
  return config;
}

https://github.com/Angular-RU/angular-cli-webpack/issues/9

monkey patching angular cli: https://github.com/Angular-RU/angular-cli-webpack

What about this must have feature? I completly agree. We need process to remove unused css even when it comes from styles.css file

Thanks @splincode

Agree with the part that software should check all that for us, but I'd argue that ideally we'd ALSO get warnings from the IDE about unused CSS (SASS, and even js), even before build time, so that we can clean up our codebase and improve maintainability. Just throwing it out there, although probably out of scope for angular-cli and even angular. Removing CSS during build is good, but that only benefits users.

Some modern CSS Frameworks such as TailwindCSS are tying on the use of a tool to remove unused CSS. The only option for that is to eject the app, but that could be avoided if such feature is added.

Need this feature!

Developers can manipulate elements directly using JavaScript which makes the automatic style removal in the general case impossible.

If we release such feature we'll break too many applications. The solution that @splincode shared will work in some cases when the ViewEncapsulation.None and no dynamic style manipulation.

Let's suppose we have:

@Component({
  selector: 'my-app',
  template: '<div class="foo"></div>',
  styles: [`
    .foo {
      color: red;
    }
    .bar {
      text-align: center;
    }
  `]
})
class AppComponent {
  ngOnInit() {
    fetch('https://example.com/styles.json')
      .then(r => r.json())
      .then(s => document.querySelector('.foo').classList.add(s.className));
  }
}

If the content of styles.json is: { "className": "bar" }, there's no way we can know ahead of time that the .bar style is used, and we'd have dropped it, which will break the application.

The best you can do is use something like codelyzer to statically analyze your app and give suggestions.

I agree with @mgechev's statement; the very nature of CSS and JavaScript makes this impossible to fix for the general case.

That being said, there is three avenue you can use as a workaround to the default behavior:

  1. use a specialized linter (such as codelyzer) that can show problems in your IDE.
  2. use a builder such as ngx-build-plus to modify the web pack configuration yourself.
  3. use a post-build script for your production build, like run purifycss, on your files directly.

@mgechev The ratio of css used dynamically, is way too low with respect to the static css.

The Good solution:
We can analyze the code to detect any style manipulation, then warn the developer to perform manual check on corresponding lines of code, so they can take responsibility.

The Better solution:
I think there is more value in automating the removal of "unused css". In case manual style manipulation is used, show the developer where this is happening in their project. Then there might be a place in the configuration to "white list" those styles and prevent removing them from final bundle.

Minimizing css is a great way to achieve small angular apps and translate better load times.
At least make it a setting inside angular.json

That's what I prototyped here http://codelyzer.com/rules/no-unused-css/.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rajjejosefsson picture rajjejosefsson  Â·  3Comments

brtnshrdr picture brtnshrdr  Â·  3Comments

delasteve picture delasteve  Â·  3Comments

donaldallen picture donaldallen  Â·  3Comments

hareeshav picture hareeshav  Â·  3Comments