angular / angular-cli

CLI tool for Angular
https://cli.angular.io
MIT License
26.76k stars 11.97k forks source link

Lazy loaded styles are never hashed #12552

Closed maosmurf closed 2 years ago

maosmurf commented 6 years ago

We are great fans of lazy-css functionality in ng build, it allows us to serve several apps from one code-base, as we add app1.css ... appN.css depending on the request, using angular universal. Given our high-traffic page we use a CDN and therefore rely on changing hashes for changed CSS, as we otherwise would have to manage CSS cache invalidation manually.

How can we have hashed lazy CSS back?

See also #11235 and #11704

Bug Report or Feature Request (mark with an x)

- [x] bug report -> please search issues before submitting
- [ ] feature request

Command (mark with an x)

- [x] build

Versions

$ node --version
v8.11.4
$ npm --version
5.10.0
$ ng --version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/

Angular CLI: 6.2.4
Node: 8.11.4
OS: darwin x64
Angular: 6.1.2
... animations, common, compiler, compiler-cli, core, http
... platform-browser, platform-browser-dynamic, platform-server
... router, service-worker

Package                            Version
------------------------------------------------------------
@angular-devkit/architect          0.0.0
@angular-devkit/architect-cli      0.0.0
@angular-devkit/build-angular      0.0.0
@angular-devkit/build-ng-packagr   0.0.0
@angular-devkit/build-optimizer    0.0.0
@angular-devkit/build-webpack      0.0.0
@angular-devkit/core               0.0.0
@angular-devkit/schematics         0.0.0
@angular-devkit/schematics-cli     0.0.0
@angular/cdk                       6.4.5
@angular/cli                       0.0.0
@angular/material                  6.4.5
@angular/pwa                       0.0.0
@ngtools/json-schema               1.1.0
@ngtools/webpack                   0.0.0
@schematics/angular                0.0.0
@schematics/schematics             0.0.0
@schematics/update                 0.0.0
ng-packagr                         4.1.0
rxjs                               6.2.2
typescript                         2.9.2
webpack                            4.16.4

MacOS 10.14 Mojave

Repro steps

my-app/angular.json -> projects/my-apparchitect/build:

{
  "input": "src/extra.css",
  "lazy": true,
  "bundleName": "extra"
}

Then run:

$ ng build --prod

Example in: https://github.com/maosmurf/ng-demo/tree/lazy-styles

The log given by the failure

chunk {4} extra.css (extra) 0 bytes [initial] [rendered]

Desired functionality

extra.123456678.css instead of extra.css, as it was until recently.

While removing the hash from lazy styles helps with lazy includes, it hinders cache invalidation.

Our use-case: We render the app (kurier.at) and have lazy styles. Given we use angular universal, we have no problem checking /dist for the current css hash file. After deploying a new version, the new css (with changed hash) is included, yet the old CSS remains valid on CDN (cloudflare) - so there are 2 benefits fron this: 1) no need for invalidating style.css 2) old CSS remains valid for old cached pages.

How can we get the old functionality back? Currently, our only option for upgrading to ng6 would be to manually add hashes after ng build --prod

clydin commented 6 years ago

This is intentional, lazy loaded styles are not referenced by the built application automatically and are only intended to be referenced by the developer. As such, a hash prevents the file from being deterministically referenced as the name would change upon every content change.

In addition, since the file is not referenced automatically within the application, the filename can be modified as necessary after the build. So as mentioned, if the project wanted a hash or some other unique identifier attached, it could do so with a post build step.

There is the potential for an additional capability in regards to either allowing template tags in bundleName or adding an additional option to enable hashing on a per file basis. I'm going to mark this as a feature request.

maosmurf commented 6 years ago

Thank you for the feedback.

This is intentional

I fully understand the motivation for the recent changes, yet the backwards-incompatible changes caused some headache.

it could do so with a post build step.

Done.

I'm going to mark this as a feature request.

Would be great to have old functionality back using already provided tools (without the need of manual post-build step), so thank you for that 👍

jc-1234 commented 5 years ago

We are running into similar issue with invalidating lazy loaded stylsheets. Would be great if there can be a config setting in angular.json to turn this on or off.

mehmet-erim commented 4 years ago

You create lazy-loadable hashed styles with Webpack. See my article about this: https://volosoft.com/blog/Extracting-and-Hashing-Lazy-Loaded-CSS-in-Angular

My Angular CLI version: 8.3.19

  1. ng new lazy-load-css --style css
  2. Open the angular.json and remove styles.css from styles array.
  3. Create style-loader.js in the src folder and then replace with the following code:
    import styles from './styles.css';
    const node = document.createElement('style');
    node.textContent = styles;
    document.querySelector('head').appendChild(node);
  4. Add the ngOnInit method in the AppComponent like below:
    ngOnInit() {
    setTimeout(() => {
      import('src/style-loader');
    }, 1500);
    }

    See Webpack import() function. Also you can see Webpack magic comments for chunk naming, etc.

  5. Add a style to styles.css to see changes. I added the background color style.
  6. Run the app

lazy-loadable-css-hashing

Run the ng build --prod to see the hash

vlodko commented 4 years ago

@maosmurf have you found any workarounds?

crhistianramirez commented 3 years ago

Our workaround for this was to create a post-build script. Here's some code that anyone is free to use, you may need to modify sourceDir. It outputs a manifest.json to the root directory so that you are able to reference your hashed filename by filename in your app at runtime.

We also used hasha for the actual hashing.

const hasha = require('hasha')
const fs = require('fs')

let manifest = {}

const sourceDir = './dist'
fs.readdirSync(sourceDir)
  .filter((filename) => filename.endsWith('.css'))
  .forEach((filename) => {
    const hash = hasha.fromFileSync(`${sourceDir}/${filename}`).substring(0, 10) // unique enough for our cache busting purposes

    const withoutExtension = filename.slice(0, -4)
    const hashedFileName = `${withoutExtension}.${hash}.css`
    fs.renameSync(`${sourceDir}/${filename}`, `${sourceDir}/${hashedFileName}`)
    manifest[filename] = hashedFileName
  })

fs.writeFileSync(
  `${sourceDir}/manifest.json`,
  JSON.stringify(manifest, null, 4)
)
bogacg commented 3 years ago

@mehmet-erim your method seems interesting. Does webpack also minify / prefix styles added this way or we have to provide a css file minified/prefixed beforehand?

mehmet-erim commented 3 years ago

@bogacg Obviously I have not checked this. I have provided minified CSS files.

poddarkhushbu07 commented 3 years ago

This feature is quite helpful because I want to refer this lazy loaded files within the application code. So, if this lazy loaded styles are hashed it would not be possible to refer these files within application code.

Can't we have any possible workaround if we want to access hashed file styles file from application code?

Case: style files which are loaded (lazily and non lazily) both needs to referenced from service.

mdarefull commented 3 years ago

I would also like to comment in favour of this feature.

On one hand, we all know how important fast load time is for Web Apps, that's the main motivation of lazy loading modules in Angular.

The problem is that if I reference big UI libraries, like Angular Material or even a specific component, it is a pain to lazy load those styles so I have to include +1Mb of content as part of my main module.

Manually hashing the file is not desired as a simple npm update could change the content and no one might remember to update the hash. If a post-build task is possible then why don't we include the hashing as default? is body-enabled so we could reference them inside any component. Why not to specify a component property inside the style object that tells the builder which component will reference the link and then it is added just as it adds to index.html...

image

clydin commented 3 years ago

The Angular material themes ideally should not be used within component styles. They are recommended to be placed in a global stylesheet via the project's styles option or directly in the index.html via a link element as a project asset. For additional information, please see https://material.angular.io/guide/theming#using-a-pre-built-theme One of the added benefits of using a global style is that Angular 11 also provides the new inlineCritical style optimization which will analyze all global styles, inline critical CSS within the HTML and then lazy load the remainder of the global styles.

mdarefull commented 3 years ago

@clydin I think that's an oversimplification of styles management.

First, most complex app won't use pre-built theme, but custom themes, either by creating their own or by customizing the pre-built ones. In addition you usually also add customizations for components that you don't want to duplicated on every angular component that uses it, like different colors, margins, sizes for tabs, list items, etc.

Where would you put those? If you import the styles in your global stylesheet you are still downloading several Kbs of styles that you don't use to bootstrap the app... if you add customizations to styles.scss to make them global, same thing... The solution here would be to leverage lazy loading styles just the same way we leverage lazy loading modules... perhaps we could even have a concept of module styles... that is similar to the global styles.scss in capabilities... but is only included when the module is loaded.

Currently, my main module bundle is sitting at 1.22Mb after optimizations, and around 30-40% of it are styles imported in the styles.scss file

clydin commented 3 years ago

Styles that affect the entire application such as theming are recommended to be placed in a global stylesheet. With the latest style optimizations enabled it will be loaded in a non-blocking manner in parallel with the initial JavaScript bundles. Styles that only affect a particular component are recommended to be placed in component specific styles (inline styles or external styleUrls). If the component is lazy loaded then these styles will also be lazy loaded. Since the component specific styles are required to be present for the component to successfully render, separately lazy loading the stylesheets for a component will increase the time necessary to initialize and render the component; as well as increase the amount of data that would need to be transferred due to the overhead of the additional file and the code necessary to bootstrap it.

Regarding the size of the main bundle, if the styles are already present in the global styles.scss, they should not need to be imported into any component styles. There should ideally not be any rule overlap between the global stylesheet and component styles.

mdarefull commented 3 years ago

@clydin global styles are included as part of the initial bundle for optimization/performance criteria.

What about styles for a Grid? Would you duplicate those styles on each component that uses the grid? Or would you add it to the global styles? If you add it to the global styles, why are we downloading KBs of styles that might never be displayed? (the grids might be part of an admin module?)

With angular material, we are able to import the modules specific to each component individually and when needed... why is then OK to import the styles for the whole library as part of the initial load?

Check a mid-size to complex app and run lighthouse.. it will tell you that a big chunk of your styles are not being used on your home page and suggest removing them to improve load time.

clydin commented 3 years ago

This Angular RFC may be of interest: https://github.com/angular/angular-cli/issues/18730 All of the recommendations have been implemented in the latest stable version of Angular. The developer relations team also put together a video demonstrating some of the features which can be found here: https://youtu.be/yOpy9UMQG-Y

For the grid question, if a grid with a particular setup (style, layout, and/or behavior) is used multiple times within an application, the recommended design pattern is to create an application specific grid component that encapsulates this setup. This component would only be loaded (including its component styles) when needed by a particular route. This also allows centralized updates of the grid appearance/behavior as well as easier enforcement of application style guidelines.

The Angular material components use both global styles (generally for theming) as well as component styles for component specific elements. With the latest versions of Angular, global stylesheets can be downloaded and processed in parallel to the initial JavaScript bundles. JavaScript module downloading, parsing, evaluation, and application bootstrapping will in the majority of cases exceed the time to download and process the global styles. Pruning of unused global styles for the application is also currently a planned feature but has not yet been implemented.

mdarefull commented 3 years ago

@clydin I think you are still oversimplifying...

You are assuming I'm the one who built the Grid... the grid component might have been built by a 3rd party library... it might actually be part of a huge component library like angular material, ng bootstrap, ag-grid, etc... Those components are very style heavy and usually provide styles as an external file and the recommendation is to either import it as part of styles.scss or add it to the styles array in angular.json.

Going back to the issue I mentioned about... if my home screen contains none of those components... why do I have to include KBs worth of unused styles as part of the initial load?? Just right now I have an enterprise app, with just couple pages and some enterprise grids... only using angular material and ag grid... those 2 styles, after optimization are 200KB... half of the bundle size warnings angular recommends for production apps.

If I want to customize all these components, the only way, AFAIK is to include the style customizations in a global stylesheet (styles.scss)... if I'm building an enterprise app with thousands of customizations... why all these customizations have to be part of the initial load?

Finally, you didn't answer one of my questions... why does Angular material fragmented their components into separate modules and allow us to lazy load them when needed, but we have to load all the library styles as part of the initial load?

clydin commented 3 years ago

The Grid could be a third-party component, first-party or even non-Angular. The encapsulation is still possible and as an application scales in size, this can become increasing beneficial. For example, if the Grid needs to be changed to a different underlying implementation, the change only needs to happen in one place and the entire application is updated. Having a set of shared components that are used throughout an application is a frequently used pattern in Angular applications.

Angular material themes can be customized to only include specific components. Please see the theming guide for additional information: https://material.angular.io/guide/theming#theming-only-certain-components If this alone does not provide a global styles file that meets the project's requirements then the component theme styles can be bifurcated with the less used placed in a relevant lazy route's main component that has view encapsulation turned off.

angular-robot[bot] commented 2 years ago

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

dgp1130 commented 2 years ago

This issue seems to have gone a bit off-topic, but to refocus on the original request of hashing lazy loaded styles...

We don't really have a feature for "lazy loaded styles" as defined here. You can bundle styles using the CLI and skip injecting them in the index.html page as is typically done. This pattern is more of an "unreferenced stylesheet" which can be manually loaded onto a page by application code. Angular doesn't do anything special with such a stylesheet after bundling it. (Sidebar: We do have critical CSS inlining which defers some styles to be loaded lazily, however this is a different feature than what's discussed here and is done transparently for developers.)

This distinction is important because appending a hash to the filename makes the reference unstable and means it can't be easily referenced by application code. By contrast, we can add hashes to JS files because the CLI typically controls all the references to them (the index.html page and dynamic imports of lazy loaded chunks), so we can make sure everything is consistent. Since the CLI doesn't have this same control over bundled styles, we have no hook to allow applications to use a stable reference to a stylesheet with a hash in the filename.

Leaving the hash out of the bundled CSS is the only way to effectively reference that file, so we think it is the right approach here. Including the hash could be useful if Angular had proper support for lazy loading styles, but that's a separate FR. If that's something that you think would be interesting and have a use case for it, then please file that FR.

angular-automatic-lock-bot[bot] commented 2 years ago

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.