Optimising Node.js applications: The impact of --max-semi-space-size on garbage collection efficiency

Optimising Node.js applications: The impact of --max-semi-space-size on garbage collection efficiency

·

4 min read

By Alfonso Graziano and Vikas Bhandari

Understanding how the V8 garbage collector works helps improve the performance of a Node.js app

Improving the throughput of a Node.js app can be a complex topic, involving code optimisations, architecture changes and a deep understanding of Node.js best practices. But making this improvement means the application can handle a larger user base without compromising performance. The benefit of this is straightforward — more users equates to a higher customer base and greater revenue potential for organisations.

In some cases, the knowledge of how the V8 garbage collector (GC) works can help in improving performance without changes in the code. In this article, we investigate a flag that allows the override of a V8 garbage collector setting: max-semi-space-size.

As per Node.js’ official documentation, the flag:

“Sets the maximum semi-space size for V8's scavenge garbage collector in MiB (megabytes). Increasing the max size of a semi-space may improve throughput for Node.js at the cost of more memory consumption.”

To understand its usage, we have to understand what a semi-space is and how it is important and used in a V8 garbage collection.

What is a garbage collector?

V8 uses a smart garbage collector to free up memory whenever required. For performance optimisation, it creates different partitions to place objects and executes a smart strategy to run the process efficiently. This approach helps the GC to save time and focus only on those objects that need immediate attention.

Generational garbage collection divides the objects into two partitions: the young generation and the old generation. The young generation is further divided into two equal parts, known as semi-spaces: nursery and intermediate.

Young generation

In a practical world, most of the objects are short-lived. For example, function-scoped objects which may be redundant once the function has completed processing become unused very quickly. V8 initially allocates the objects in the “nursery” (active semi-space) partition of “young generation”. This is so that in case an object becomes redundant in a short time span, it can be quickly removed in the garbage collector cycle run. When objects get removed from the young generation, it barely impacts the event loop.

“Young generation” holds relatively small memory by default (up to 16 MB) and since the current world is filled with memory-intensive applications, as a result, GC runs frequently to free up the memory here.

Here is an example: In the garbage collector cycle Run #1, if the objects are unused, the GC frees up the memory by eliminating the objects, and the remaining objects are moved from nursery to intermediate. In Run #2, if the objects still survived in intermediate, then these objects are further moved into the old generation.

Node’s command line options include a command line flag --max-semi-space-size to set the memory size (in MBs) that can be allocated to the semi-space. If we increase the allocated size of semi-space, it will be able to hold more data, resulting in an increased throughput with a trade-off with increased memory utilisation.

For this example, we created a simple Fastify application with a single route which allocates a large number of objects.

const fastify = require("fastify")();

const allocateHeavyObjects = () => {
  const numberOfObjects = 1000000;
  const objects = [];
  for (let i = 0; i < numberOfObjects; i++) {
    objects.push({ index: i, timestamp: Date.now(), random: Math.random() });
  }
  return objects;
};

fastify.get("/heavy", async (request, reply) => {
  allocateHeavyObjects();

  reply.send({
    message: "finished heavy request",
  });
});

fastify.listen({ port: 3000 })

Running a test with Autocannon (10 seconds with 10 connections) using autocannon <http://localhost:3000/heavy, we can see some different results:

  • Default settings: 14.5 Req/sec

  • -max-semi-space-size=128: 17.4 Req/sec (20% more than the baseline)

  • -max-semi-space-size=256: 18.5 Req/sec (27% more than the baseline)

Conclusion

In most cases, you will not need to change the semi-space settings as V8 engine’s smart Parallel Scavenge is really efficient in doing a parallel GC. That being said, for some specific allocation patterns, testing multiple values for the max-semi-space-size flag may be beneficial for the overall application throughput.

In conclusion, understanding and optimizing the V8 garbage collector's settings, specifically through the max-semi-space-size flag, can have a notable impact on the efficiency of garbage collection and, by extension, the performance of a Node.js application without altering the application code.