Lazy Loading Scripts and Styles in Angular

In my earlier post, I covered about lazy loading of angular modules. In this post, I will cover lazy loading of scripts and styles, both external and local. The goal of this post is to show how you can defer loading of a script or style and load it manually later when you need. This also works perfectly with external scripts that if added to the index.html file of your project would have a negative impact on performance.

It works well with styles and scripts that are needed for a specific case. One such scenario that spring to mind is handling payment using PayPal. In most cases, payment is usually processed only after a customer has order an item not before. In this case, loading PayPal by placing the script tags at the index.html would negatively impact performance while it’s only needed at the last step for payment processing.

Loading Scripts and Styles on Demand

You will most like be loading the scripts in a component. To load the style or script, we are going to use document.CreateElement method to add a new script/style element. For the script, we will create new scripts tags with the source pointing to our script URL. For the style,  we will create new link element in the header, with HREF to our style URL.

To load our script, we begin by creating a new method loadExternalScript(scriptURL),  which will return a promise. A promise will make it easier to ensure that we will use our script after it has been loaded successfully and not before.

return new Promise(resolve => {
  const scriptElement = document.createElement('script');
  scriptElement.src = scriptUrl;
  scriptElement.onload = resolve;
  document.body.appendChild(scriptElement);
});

The same goes for our style, where we will create a new method and then return a promise:

private loadExternalStyles(styleUrl: string) {
  return new Promise((resolve, reject) => {
    const styleElement = document.createElement('link');
    styleElement.href = styleUrl;
    styleElement.onload = resolve;
    document.head.appendChild(styleElement);
  });
}

You can wrap the above two methods in a class or service if you wish. It will make it easier to reuse the methods all over your class easily. Then, call the methods to load the scripts/style as show below:

this.loadExternalScript('url/to/your/scripts').then(() => {}).catch(() => {});
this.loadExternalStyles('url/to/your/styles).then(() => {}).catch(() => {});

And that’s it. You can now load styles/scripts on demand and not before. In the next step, I am going to cover how to lazy load local scripts.

Lazy Loading Local/Internal Scripts and Styles

When Angular CLI is building your application, all the styles and scripts added to the angular.json file are automatically added to the index.html, meaning they will be preloaded before rendering.

"architect": {  
 "build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": {
      "styles": [
        "src/styles.css"
      ],
      "scripts": [
        "src/some-script.js"
      ],
      ...

To avoid this, you can specify that a specific script/style is to be lazy loaded and angular will just output a bundle and won’t add it to the index.html. You do this by specifying the script/style in object format, instead of a simple string as shown above.

The object formats allows you to provide more information to the CLI about how to handle a specific style sheet or script. This information includes the output name, whether to lazy load and the source of the style/script.

{
  "input":"path/to/your/lazy-script.js",
  "lazy": true,
  "bundleName": "lazy-loaded-script"
},

You also must specify the output name (bundleName) to be able to reference to it later. If you don’t, Angular CLI will randomly generate a bundle name, which will change on each build.

"scripts": [
  "src/some-script.js",
  { 
     "input": "path/to/your/lazy-script.js", 
     "lazy": true, 
     "bundleName": "lazy-loaded-script"
  },
],
"styles":
 [
  "src/some-styles.css",
  {
    "input": "path/to/your/lazy-style.css", 
    "lazy": true, 
    "bundleName": "lazy-loaded-style"
  },
],

3 Replies to “Lazy Loading Scripts and Styles in Angular”

  1. This is not working in angular 6.
    angular when we ng serve then it is not creating .css file instead it create .js file for css file.

    package.json
    {
    “name”: “lazyload”,
    “version”: “0.0.0”,
    “scripts”: {
    “ng”: “ng”,
    “start”: “ng serve”,
    “build”: “ng build”,
    “test”: “ng test”,
    “lint”: “ng lint”,
    “e2e”: “ng e2e”
    },
    “private”: true,
    “dependencies”: {
    “@angular/animations”: “^6.1.0”,
    “@angular/common”: “^6.1.0”,
    “@angular/compiler”: “^6.1.0”,
    “@angular/core”: “^6.1.0”,
    “@angular/forms”: “^6.1.0”,
    “@angular/http”: “^6.1.0”,
    “@angular/platform-browser”: “^6.1.0”,
    “@angular/platform-browser-dynamic”: “^6.1.0”,
    “@angular/router”: “^6.1.0”,
    “core-js”: “^2.5.4”,
    “rxjs”: “^6.0.0”,
    “webpack”: “^4.16.5”,
    “zone.js”: “~0.8.26”
    },
    “devDependencies”: {
    “@angular-devkit/build-angular”: “~0.7.0”,
    “@angular/cli”: “~6.1.2”,
    “@angular/compiler-cli”: “^6.1.0”,
    “@angular/language-service”: “^6.1.0”,
    “@types/jasmine”: “~2.8.6”,
    “@types/jasminewd2”: “~2.0.3”,
    “@types/node”: “~8.9.4”,
    “codelyzer”: “~4.2.1”,
    “jasmine-core”: “~2.99.1”,
    “jasmine-spec-reporter”: “~4.2.1”,
    “karma”: “~1.7.1”,
    “karma-chrome-launcher”: “~2.2.0”,
    “karma-coverage-istanbul-reporter”: “~2.0.0”,
    “karma-jasmine”: “~1.1.1”,
    “karma-jasmine-html-reporter”: “^0.2.2”,
    “protractor”: “~5.3.0”,
    “ts-node”: “~5.0.1”,
    “tslint”: “~5.9.1”,
    “typescript”: “~2.7.2”
    }
    }

    angular.json
    {
    “$schema”: “./node_modules/@angular/cli/lib/config/schema.json”,
    “version”: 1,
    “newProjectRoot”: “projects”,
    “projects”: {
    “lazyload”: {
    “root”: “”,
    “sourceRoot”: “src”,
    “projectType”: “application”,
    “prefix”: “app”,
    “schematics”: {},
    “architect”: {
    “build”: {
    “builder”: “@angular-devkit/build-angular:browser”,
    “options”: {
    “outputPath”: “dist/lazyload”,
    “index”: “src/index.html”,
    “main”: “src/main.ts”,
    “polyfills”: “src/polyfills.ts”,
    “tsConfig”: “src/tsconfig.app.json”,
    “assets”: [
    “src/favicon.ico”,
    “src/assets”
    ],
    “styles”: [
    {
    “input”: “src/styles.css”,
    “lazy”: true,
    “bundleName”: “style”
    }
    ],
    “scripts”: [
    “src/assets/bootstrap/jquery-3.2.1.slim.min.js”,
    {
    “input”: “src/assets/bootstrap/bootstrap.min.js”,
    “lazy”: true,
    “bundleName”: “bootstrap”
    }
    ]
    },
    “configurations”: {
    “production”: {
    “optimization”: true,
    “outputHashing”: “none”,
    “sourceMap”: true,
    “extractCss”: true,
    “namedChunks”: false,
    “aot”: true,
    “extractLicenses”: true,
    “vendorChunk”: false,
    “buildOptimizer”: true,
    “fileReplacements”: [
    {
    “replace”: “src/environments/environment.ts”,
    “with”: “src/environments/environment.prod.ts”
    }
    ]
    },
    “serve”: {
    “optimization”: true,
    “outputHashing”: “none”,
    “sourceMap”: true,
    “extractCss”: true,
    “namedChunks”: false,
    “aot”: true,
    “extractLicenses”: true,
    “vendorChunk”: false,
    “buildOptimizer”: true
    }
    }
    },
    “serve”: {
    “builder”: “@angular-devkit/build-angular:dev-server”,
    “options”: {
    “browserTarget”: “lazyload:build”
    },
    “configurations”: {
    “production”: {
    “browserTarget”: “lazyload:build:production”
    }
    }
    },
    “extract-i18n”: {
    “builder”: “@angular-devkit/build-angular:extract-i18n”,
    “options”: {
    “browserTarget”: “lazyload:build”
    }
    },
    “test”: {
    “builder”: “@angular-devkit/build-angular:karma”,
    “options”: {
    “main”: “src/test.ts”,
    “polyfills”: “src/polyfills.ts”,
    “tsConfig”: “src/tsconfig.spec.json”,
    “karmaConfig”: “src/karma.conf.js”,
    “styles”: [
    “src/styles.css”
    ],
    “scripts”: [],
    “assets”: [
    “src/favicon.ico”,
    “src/assets”
    ]
    }
    },
    “lint”: {
    “builder”: “@angular-devkit/build-angular:tslint”,
    “options”: {
    “tsConfig”: [
    “src/tsconfig.app.json”,
    “src/tsconfig.spec.json”
    ],
    “exclude”: [
    “**/node_modules/**”
    ]
    }
    }
    }
    },
    “lazyload-e2e”: {
    “root”: “e2e/”,
    “projectType”: “application”,
    “architect”: {
    “e2e”: {
    “builder”: “@angular-devkit/build-angular:protractor”,
    “options”: {
    “protractorConfig”: “e2e/protractor.conf.js”,
    “devServerTarget”: “lazyload:serve”
    },
    “configurations”: {
    “production”: {
    “devServerTarget”: “lazyload:serve:production”
    }
    }
    },
    “lint”: {
    “builder”: “@angular-devkit/build-angular:tslint”,
    “options”: {
    “tsConfig”: “e2e/tsconfig.e2e.json”,
    “exclude”: [
    “**/node_modules/**”
    ]
    }
    }
    }
    }
    },
    “defaultProject”: “lazyload”,
    “schematics”: {
    “@schematics/angular:component”: {
    “prefix”: “app”,
    “styleext”: “scss”
    },
    “@schematics/angular:directive”: {
    “prefix”: “app”
    }
    }
    }

    1. Hi, to save time, Angular bundles styles as JS when you ng serve to make hot reloading fast. But it should be working with extractCss set to true on ng server, try issuing the command while serving:

      ng server --extract-css --aot

      And see if it works. Am also working on it to see if i can get a solution.

    2. Like i said in my previous comment, the process works on ng build but if you want it to work on ng serve, please add "extractCss": true under options, under build, under architect.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.