Using Angular CLI to Speed Up Development

Anton Ioffe - November 27th 2023 - 11 minutes read

In the relentless pursuit of excellence within modern web development, Angular CLI has emerged as the pivotal cog in the developer's powerhouse of tools. Beyond mere syntax, it orchestrates the complex symphony of project scaffolding, build optimization, and strategic performance enhancements. This article forges a path through the intricate capabilities of Angular CLI, shedding light on the profound ways it can expedite the development workflow from conception to completion. As senior-level developers, you will not only understand the nuances of deploying pivotal features like angular.json configurations, watchers, or incremental builds but also master future-proof techniques such as AoT compilation and lazy loading. Prepare to transform your development expertise into a velocity so compelling that it keeps you at the cutting edge of efficiency and innovation.

Embracing Angular CLI: Drastically Enhance Your Workflow

Angular CLI, an indispensable facet of the Angular ecosystem, is a powerhouse that turbocharges development workflows. At its core, the CLI provides robust project scaffolding capabilities, enabling developers to swiftly generate the skeletal structure for new Angular projects. This automated setup is more than a convenience—it's a foundational step that guarantees consistency across projects and adherence to Angular's best practices. By meticulously defining the files, folders, and configurations needed for a project, Angular CLI eradicates the drudgery of manual setup. This uniformity is paramount, especially when scaling up development teams or managing multiple projects, thus ensuring that every developer speaks the same structural language right from the start.

The true supremacy of Angular CLI lies in its adeptness at context-aware code generation. With Angular proliferating in complexity, creating components, directives, services, and pipes by hand becomes not only tiresome but error-prone. Angular CLI standardizes these elements with preconfigured, boilerplate code snippets summoned with succinct commands. For instance, when you generate a service using the CLI, it promptly creates the service file, provides it with a boilerplate injectable decorator, and updates the requisite module. This streamlines productivity, slashes potential mistakes, and accelerates the development of feature-rich applications.

In terms of enhancing the environment setup, Angular CLI is the linchpin that pairs with Node.js to furnish a seamless development server that reflects real-time progress. It transpiles TypeScript, marshals assets, and optimizes the build process, thereby creating an ecosystem where developers can focus purely on coding. The seamless experience Angular CLI provides during the development phase is invaluable; it allows for instant feedback on changes and diminishes the feedback loop, drastically improving developer experience and efficiency.

Angular CLI's commitment to automation and best practices goes beyond mere file generation—it propels the entire development lifecycle forward. The encapsulated build tools, testing frameworks, and deployment scripts it provisions means that from the moment you instantiate a new project to the point you deploy, Angular CLI is a companion that simplifies complexity. It deliberately conceals the intricacies of configuration and tool-chain management, allowing developers to pivot their concentration to crafting innovative code rather than wrestling with setup and maintenance tasks.

To contextualize the transformative power of Angular CLI, one should consider common coding mistakes it helps to circumvent. For example, a developer might inadvertently miss declaring a generated component in a module, causing runtime failures. Angular CLI's code generation commands inherently encapsulate the component declaration, obviating such an oversight. It's this intelligent scaffolding and automation that significantly truncates error rates, propelling development velocity. Moreover, the CLI's uniform conventions instigate a codebase that is readable, modular, and primed for reusability—a trifecta that every senior developer aspires to actualize. With Angular CLI, the tedious becomes trivial, the error-prone becomes robust, and the complex becomes manageable, leading to a workflow that epitomizes efficiency and less technical debt.

Command Line Mastery: Supercharging Your Angular Builds

Harnessing the Angular CLI's command-line options can yield drastic improvements in build efficiency. Perhaps the most well-known flag, --prod, activates production optimizations, including Ahead-of-Time (AoT) compilation, minification, and uglification. While it's true that these processes intensify build times, the trade-off comes in the form of significantly reduced bundle sizes and improved runtime performance. Another compelling flag for production is --build-optimizer, which works in conjunction with --prod to further eliminate unnecessary runtime code and trim down bundle sizes. These flags are pivotal in developing applications that perform seamlessly on a production level.

In the realm of development builds, one must weigh the necessity of faster rebuild times against the sanity of checking the entire codebase. Flags like --named-chunks and --vendor-chunk are influential in maintaining cache coherence across builds, facilitating quicker application reloads and smoother development experience. Employing --source-map can be a double-edged sword: while crucial for debugging, it can also inflate build times, so toggling this option when not actively debugging could shave off valuable seconds.

ng build --prod --build-optimizer=false

A less visible, yet equally important tweak is using the NG_PERSISTENT_BUILD_CACHE=1 environment variable or leveraging Angular 13's cache property in angular.json. This caching mechanism utilizes disk space to cache parts of the build pipeline, enabling significantly faster incremental builds. For Angular versions below 13, manually setting this environment variable can lead to noteworthy performance gains. While this might consume additional disk resources, developers constrained by build times may find the trade-off sensible.

NG_PERSISTENT_BUILD_CACHE=1 ng build

The potency of module federation can also not be overstated. By leveraging this paradigm, large projects can be split into smaller, more manageable chunks that can be built separately, reducing the overall build time. This technique fosters better scalability and maintainability by dividing the application into distinct domains or feature sets, allowing teams to work in a more decoupled and parallelized manner.

Performance profiling is an integral part of optimizing your build process. Angular CLI provides a way to measure the build times and identify bottlenecks through build statistics via the NG_BUILD_PROFILING=1 flag. The generated stats can guide developers on where to focus their optimization efforts, thereby streamlining the build process. It's essential to profile periodically to ensure that no regressions are introduced as the codebase evolves.

NG_BUILD_PROFILING=1 ng build --prod

These command line tactics, alongside diligent profiling, can supercharge your Angular builds, striking a balance between development agility and production efficiency. It's crucial to evaluate the impact of each flag in the context of your project requirements to harness the CLI's full potential and push the boundaries of what's achievable with Angular's robust ecosystem.

From Config to Completion: Leveraging angular.json for Peak Performance

Angular's angular.json is the cockpit of the project's configuration, providing powerful levers for tuning the performance of your build process. Delving into its properties reveals numerous options for tweaking build speeds. For instance, adjusting the aot property can drastically alter the compilation time. While aot: true can prepare your application for faster rendering at runtime, setting it to false during development can significantly speed up the build process. Here's where understanding your application's needs during various stages of development becomes crucial, as AOT adds to build time but offers runtime performance benefits.

...
"architect": {
    "build": {
        "builder": "@angular-devkit/build-angular:browser",
        "options": {
            ...
            "aot": false,
            ...
        },
        "configurations": {
            "development": {
                ...
            }
        }
    }
}
...

Modifying the configurations under the development key opens optimization channels. Developers often overlook flags such as optimization, outputHashing, and sourceMap which can be set to false to accelerate the build during the development phase. Disabling code optimizations and source maps reduces the build time, but require a balance between performance and the ability to debug efficiently.

"development": {
    "optimization": false,
    "outputHashing": "none",
    "sourceMap": false,
    ...
},

Complexity and maintainability play a pivotal role when working with angular.json. Keeping configurations simple and avoiding custom build steps unless absolutely necessary are key. It’s enticing to manually string together processes for specific needs like localization, but doing so can add an unexpected toll on build times and complexity. Trusting Angular’s built-in systems alleviates the risk of inadvertently decelerating your build process.

Let's address common missteps that can inadvertently sabotage performance. Failing to use code-splitting, which is a practice often manually maintained by developers, can lead to bloated bundles and sluggish apps. Route-level and component-level code-splitting can be strategically set up in Angular routes configuration, reducing the initial payload and speeding up app load times.

const routes: Routes = [
    {
        path: 'feature',
        loadChildren: () => 
            import('./feature/feature.module').then(m => m.FeatureModule)
    }
];
...

Lastly, although not a part of angular.json, neglecting to rethink your deployment strategy with each iteration can impact performance. Constantly revising how your CI/CD pipeline delivers updates and integrates with services like CDNs can have a positive effect on how your application is received by the end user. Regularly reviewing and refining processes outside the direct scope of Angular’s configuration can contribute to overall performance gains.

Continuous Improvement: Streamlining Development with Watchers and Incremental Builds

Angular CLI's --watch flag revolutionizes the development experience by enabling a watch mode that observes file changes and triggers incremental builds. When ng build --watch is invoked, developers can work with a live-updating application where only modified chunks of code are rebuilt. This feature significantly reduces the time developers spend waiting for the entire application to rebuild when making small changes. Notably, incremental builds under watch mode are optimized for a development environment and may not represent the full suite of optimizations applicable to production builds.

The integration of Hot Module Replacement (HMR) further enhances the agility of the development process. With HMR, Angular can inject updated modules directly into the running application without requiring a full page reload. As a result, preserving the state of the application during development becomes seamless and speedy. However, developers must wield HMR with care, especially when dealing with stateful operations like RxJS subscriptions or WebSocket connections. Incorrect handling of such features in conjunction with HMR may lead to duplicate connections or inscrutable errors.

While using watch mode and HMR can accelerate development, there are trade-offs. The immediacy of reflecting changes and maintaining state can sometimes lead to complacency in managing the application's state architecture. Developers must vigilant about properly cleaning up and releasing resources during module replacement to prevent resource leaks and unintended side effects. Additionally, reliance on incremental builds for development can occasionally mask issues that only surface during a full build process, such as tree shaking effectiveness and module dependency resolution.

Employing small, well-defined Angular modules plays a critical role in optimizing incremental builds. When a project is organized into smaller modules, the watch mode only needs to rebuild the module that contains the changed file, thus reducing compilation time even further. Yet, it imposes a structure that may require deliberate planning and discipline to ensure modules are cohesive and maintainable.

Pros:

  • Decreases feedback loop time by rebuilding only modified chunks of code.
  • HMR updates applications on the fly, preserving state and speeding up development.

Cons:

  • Potential for overlooking cleanup of stateful resources, which can cause memory leaks or errors.
  • May give a false sense of security, as issues that occur on a full build can be missed.

In conclusion, the critical integration of watch mode and HMR into a developer’s toolkit can greatly enhance the pace and efficiency of building Angular applications. With these tools, developers are empowered to maintain a constant feedback loop, essential for agile coding and iterative improvement. However, a balance must be struck; while these features offer remarkable benefits in development speed, developers should remain attentive to resource management and ensure that changes also play well in a production build scenario.

Future-Proofing With Ahead-of-Time (AoT) Compilation and Lazy Loading

Ahead-of-Time (AoT) compilation is an essential performance optimization technique in Angular that pre-compiles application components and templates before the browser downloads and runs the code. This approach contrasts with Just-in-Time (JIT) compilation, which occurs at runtime in the browser. AoT significantly enhances startup performance as the browser can immediately render the pre-compiled version of the application, bypassing the costly compilation step. This process not only speeds up the application's responsiveness but also improves the overall user experience, especially on mobile devices with potentially limited computational power. Importantly, AoT can and is being used during development to provide developers with faster feedback regarding their changes, despite its association with longer build times.

While AoT boosts the initial load time, lazy loading modules complement this by optimizing runtime performance. Lazy loading is the practice of loading modules of code only when they are required, typically tied to route navigation. By dividing the application into feature modules and loading them on-demand, a substantial reduction is achieved in the initial bundle size, leading to faster startup times. While navigating the app, users load only the modules related to the features they interact with, rather than downloading the complete codebase upfront. This strategic partitioning of code aligns seamlessly with Angular's modular design, contributing to efficient payload management and network utilization.

Implementing AoT and lazy loading requires a judicious balance of code organization and module structuring. Consider the following real-world code snippet, which organizes an Angular application into feature modules and configures the router to lazily load them:

const appRoutes: Routes = [
    {
        path: 'dashboard',
        loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
    },
    // other feature modules
];

@NgModule({
    imports: [
        RouterModule.forRoot(appRoutes)
    ],
    // other module metadata
})
export class AppModule {}

Here, the router configuration uses the loadChildren function to dynamically import the DashboardModule. This example typifies the modularity and reusability of Angular applications. However, a common coding mistake is to include the feature module in the main bundle by importing it statically at the application root. Doing so negates the benefit of lazy loading, as shown in the incorrect counterpart below:

// Incorrect - static import resulting in bundling DashboardModule in the main bundle
import { DashboardModule } from './dashboard/dashboard.module';

@NgModule({
    imports: [
        DashboardModule, // Shouldn't statically import here
        RouterModule.forRoot(appRoutes)
    ],
    // other module metadata
})
export class AppModule {}

Developers must remain vigilant against such errors to maintain the performance benefits of these advanced techniques.

In the case of Angular Ivy, the latest compilation and rendering pipeline, tree-shaking becomes even more impactful, as it works in tandem with AoT to eliminate unused code. This process ensures that only necessary code is bundled, decreasing the size even further. By leveraging tree-shaking efficiently alongside AoT and lazy loading, developers can structure applications to be future-ready, optimizing for upcoming user devices and standards.

It is essential to understand that AoT does not come without its trade-offs. While it reduces runtime execution, build times can increase due to the complexity of analyzing and compiling the entire application upfront. However, this is a practiced trade-off within the Angular community. Developers must strike a balance between longer build times and the performance benefits of AoT, not only for production environments but also during development. By configuring AoT for development builds and exploiting incremental build strategies, developers can mitigate some of the build time impacts while still reaping the benefits in terms of application performance. What strategies could you deploy in your development process to maintain the advantageous edge that AoT compilation provides, without significantly impacting your development loop?

Summary

The article "Using Angular CLI to Speed Up Development" explores the different ways in which Angular CLI can enhance the development workflow. The key takeaways include the importance of project scaffolding, code generation, environment setup, and leveraging command-line options for improved build efficiency. The article also highlights the role of angular.json in optimizing performance, the benefits of using watchers and incremental builds, and the significance of techniques like AoT compilation and lazy loading. The challenging technical task for the reader is to reconsider their own deployment strategy and evaluate how their CI/CD pipeline integrates with services like CDNs to improve overall performance.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers