Reducing Angular startup time: practical steps
If you're working on a substantial Angular application, you've likely noticed that the time it takes for the app to become interactive on the first load can be a concern.
Tools like the Performance Insights panel in Chrome (an excellent way to examine network activity, layout shifts, rendering behavior, TTI (time to interactive), FCP (First Contentful Paint), and more, complete with Google's own recommendations for addressing common bottlenecks), or the Lighthouse extension, will often show a TTI that leaves room for improvement. That's a significant problem, since a large portion of users will abandon a site that takes more than a few seconds to load.
Numerous strategies exist for tackling this, and many of them fall outside the frontend's scope. The focus here is specifically on Angular-related optimizations that frontend engineers can apply.
1. Prioritize lazy loading
In a large codebase, the main bundle can grow quite large when most modules are eagerly imported. This directly impacts script execution time, which in turn delays the point at which the page becomes interactive.
Angular's built-in solution here is to lazy load your routes, ideally most or all of them. This ensures the browser only receives the code chunks required for the initial view, rather than the entire application. This complements the tree-shaking that Angular Ivy performs; the framework removes its own unused code, but you must also prevent your application's unused modules from being in the initial load.
Additionally, consider creating a custom preloading strategy. This allows you to fetch the remaining route chunks in the background after the initial render. You could even prioritize the modules that are most frequently visited if you have telemetry data to inform that decision.
2. Separate the vendor chunk
It's worth noting that the official Angular documentation does not recommend this for production builds.
However, consider the following scenario: if your deployment process sends the build output to a CDN, the vendor libraries often remain unchanged between releases. This means they can continue to be served from the user's cache, leading to faster loads. Meanwhile, your application bundle would only contain your specific logic, making it smaller and quicker to fetch.
Whether this makes sense for your project is debatable. This StackOverflow thread has a solid discussion of the trade-offs involved.
3. Keep APP_INITIALIZER light
The APP_INITIALIZER injection token causes the Angular bootstrap process to pause until all associated callbacks have completed.
This can be a tempting place to put a series of dependent asynchronous calls that establish the global application state. This is a pattern to avoid, because it holds up the entire application startup, pushing the interactive time further out for the user.
If removing work from APP_INITIALIZER would require a major refactor, consider adding a caching layer. In this approach, the initializer could use cached data if available, and kick off a background request to refresh that data without blocking the app initialization. This is less disruptive than a full refactor, but it does mean you'll need to update the app's state after that background fetch completes.
4. Run and cache startup requests efficiently
A simple performance audit is to inspect the Network tab during the initial page load. Look at the sequence of API calls. You might find a chain of requests where one can't start until another finishes. See if any of these can be fired off concurrently to shorten the overall waterfall effect, especially if they are blocking the render of critical content.
For static assets, look into delegating their delivery to service workers. Additionally, if certain large API responses don't change frequently, you could configure a service worker to cache those as well. This guide can be a helpful starting point for that.
5. Inspect your bundle composition
The webpack-bundle-analyzer package is a useful tool for this. When you build your Angular project for production with the stats-json flag, you can feed the generated stats.json file into this package to visualize the contents of your bundles.
Running the analyzer opens an interactive map of your bundle in a browser. You can see exactly what is within main.js, the proportion in vendor.js when you've made that split, and what code is being lazy loaded. This makes it much easier to track your progress as you work to reduce bundle size. Furthermore, you can identify large libraries within a chunk and determine if they could be loaded on demand, thereby keeping them out of the initial bundle.
6. Measure real-world performance with telemetry
Local checks with Lighthouse or Performance Insights can be very misleading. The performance your users experience in a live environment can be vastly different due to factors outside your control, such as varied internet connections and different levels of device hardware.
The solution is to implement telemetry directly into your application. Azure Application Insights is a well-regarded option for this purpose. This guide offers a clear path for integrating it into an Angular app.
After setting up the SDK, you need to send data. The browser's Performance API provides the metrics you'll want to track, and PerformancePaintTiming might be sufficient. A detailed walkthrough on capturing these metrics and querying them in Application Insights is planned. In the interim, this article provides a good example of how to track and query page load times for your routes.
In the end, many factors that contribute to slow user experiences are beyond the frontend team's control, such as sluggish APIs, suboptimal server settings, or scaling problems. Yet for extensive enterprise applications, there's a great deal that can be done on the frontend side to guarantee acceptable load times.
Should you have any feedback or corrections on these strategies, feel free to share—collective learning is valuable.


