How to make Angular universal (SSR) load faster ? ๐Ÿš€ ๐Ÿš€

How to make Angular universal (SSR) load faster ? ๐Ÿš€ ๐Ÿš€
Photo by Kelly Sikkema / Unsplash
TLDR: Use caching middleware in the express server

Problem:

Why Angular universal application takes time to load when served using @ng-universal/express-engine and sometimes very slow

Solution with some history:

Recently we had a project for an Angular e-commerce website that should be optimized for SEO, and the assignment was to add Server side rendering so the HTML page is loaded for search engines to index.

Like any intelligent well taught developer I googled โ€œAngular Server side renderingโ€, and the first result was the Angular documentation about their @ng-universal/express-engine.

We followed the guide and migrated the application to angular universal.

Now we have an application that is rendered in the backend using express-engine (NodeJS) and then served to the final user. But as you can guess itโ€™s very slow.

we need to wait for about 11 seconds for the landing page to show when using Angular SSR.

The first thing we did is we tried to minify the main bundle (app.module.ts)

We removed all unused dependencies and added lazy loading to the routes.

We jumped from 11 seconds to 8. Good? Yeah somehow but still slow.

The second idea We had is to remove unused dependencies (We had a lot)

so we removed about 6 unused dependencies from package.json and removed some unused scripts and styles from angular.json.

Results? Iโ€™m now at 6 seconds. Nice? Yes but still, the target is to load the page in milliseconds.

So now we are done with the frontend as Iโ€™ve done all the optimizations that we could.

In the NodeJS server, we enabled gzip compression.

const compression = require('compression');
server.use(compression({level: 8}));

That made me load the homepage in 4 seconds instead of 6. Great.

As we couldnโ€™t find any other optimizations that we can apply, what is left to do is enable the cache.

Cache me:

Angular universal uses a NodeJS Express server for serving the application's final HTML page and uses the express engine to render the requested route.

What we need to do is generate the requested route once, and then store the generated page in a cache and then return it the next time a user requests the same route.

The code:

We need to edit the server.ts file generated when adding angular-universal to the project and adding an express middleware to intercept the request and implement the caching mechanism, we used a memory cache cache-manager.

server.ts (Before)

server.get('*', (req, res) => {
      res.render(
        indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: req.baseUrl}]};

server.ts (After)


const cacheManager = require('cache-manager');

const memoryCache = cacheManager.caching({
  store: 'memory',
  max: 50
});

// Serving server rendered routes
server.get('*',
  (req, res, next) => {
    // A middleware to check if cached response exists
    console.log("Looking for route in cache: " + req.originalUrl);
    // try to get the requested url from cache
    memoryCache.get(req.originalUrl).then(cachedHtml => {
      if (cachedHtml) {
        console.log("Page found in cache, 
        rendering from cache : " + req.originalUrl);
        // Cached page exists. Send it.
        res.send(cachedHtml);
      } else {
        // Cached page does not exist. 
        // Render a response using the Angular express engine
        next();
      }
    }).catch(error => {
      // if we have an error render using angular univesal
      console.error("memoryCache.get error: ", error);
      next();
    });
  },
  (req, res) => {
    // When we are at this point, it means this is 
    // the first time this route is requested
    console.log(`No cache found, rendering using universal engine`);
    res.render(
      indexHtml, {req, providers: 
      [{provide: APP_BASE_HREF, useValue: req.baseUrl}]},
      (err: Error, html: string) => {
        // After angular universal express engine renders the page 
        // we cache it so the next requests for this route will be 
        // served faster using the middleware defined earlier
        console.log("Page rendered for the first time , 
                    caching url: " + req.originalUrl)
        // Cache the rendered `html` for this request url to 
        // use for subsequent requests
        // Cache the rendered page and set the cache to be 
        // eviced after 300s (5 minutes)
        memoryCache.set(req.originalUrl, html, 300)
        .catch(err => console.log('Could not cache the request', err));

        res.send(html);
      }
    );
  }
);

And here we just need to load the page once and the next request will be loaded from the cache in no time.

After applying the cache the homepage now is loaded in 471ms.

Keep Coding everyone.

Sponsorship