Skip to content

How to make Express 2x faster #15

Open
@andrehrferreira

Description

@andrehrferreira

Hello friends of the community, how are you?

First of all, I always like to make it clear that I simply love the Express project. I have been using it in my projects for over 8 years. Therefore, this post seeks to help improve the project even more. I recently opened an inquiry regarding the performance of Express compared to other HTTP server options available. The focus on the release of version 5 was mentioned first. I fully agree that there are several priority issues in the project. However, on my own initiative, I began to study the codes more deeply to try to understand what causes Express to have a lower performance than Fasfity or Koa, for example. So I started a reinterpretation by implementing the same Express functions in a new project. In my specific case, my focus is on integrating Vite functionalities into my HTTP server and creating other layers of abstraction such as decorators. However, using Express as a base, during the development of this project I realized that the biggest performance problem that Express faces is related to the use of 'Object.defineProperty', I'll explain why.

Both Koa and Fastify use an approach of creating a new Request and Response object, defining getters and applying the objects generated by HTTP and HTTP2 from nodejs as a 'raw', assigning them as a simple property, through the getters retrieving the necessary data such as headers, body, params and queries, Express assigns getters dynamically in both the request and response using the "defineGetter" function, I understand that the way it was done takes advantage of the entire structure of original properties and functions, but the processing cost to dynamically add these getters is very high, even using Reflect.defineProperty as an alternative there is still a delay that considerably reduces the amount of requests that Express can process per second.

To make it clearer, I'll leave a simple test comparing the two approaches:

For the test I am using my personal computer a Core i9 10980XE, with 256GB DDR4, Sansung NVME SSD, on Windows 10, using WLS from Ubuntu 22.04, Node in version 20.17.0, Autocannon in version 7.15.0

autocannon -w 8 -d 10 -c 1024 http://localhost:3000

Object Define Property

const http = require('http');

const server = http.createServer((req, res) => {
  
  Object.defineProperty(req, 'xhr', {
    configurable: true,
    enumerable: true,
    get: function () {
        var val = this.headers["X-Requested-With"] || '';
        return val.toLowerCase() === 'xmlhttprequest';
    }
  });
  
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ xhr: req.xhr }));
});

server.listen(3000, () => {
  console.log('Server with Object.defineProperty is running on http://localhost:3000');
});

Result:

Stat 1% 2.5% 50% 97.5% Avg Stdev Min
Req/Sec 18,447 18,447 21,551 23,135 21,518.4 1,202.72 18,441
Bytes/Sec 3.43 MB 3.43 MB 4.01 MB 4.3 MB 4 MB 224 kB 3.43 MB

Object Create

const http = require('http');

const request = {
    get xhr() {
        var val = this.req.headers["X-Requested-With"] || '';
        return val.toLowerCase() === 'xmlhttprequest';
    }
}

const server = http.createServer((req, res) => {
    let obj = Object.create(request);
    obj.req = req;
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ xhr: obj.xhr }));
});

server.listen(3001, () => {
  console.log('Server with Object.create is running on http://localhost:3001');
});

Result:

Stat 1% 2.5% 50% 97.5% Avg Stdev Min
Req/Sec 20,447 20,447 25,359 26,383 24,836.8 1,658.27 20,440
Bytes/Sec 3.8 MB 3.8 MB 4.72 MB 4.91 MB 4.62 MB 309 kB 3.8 MB

Note that in the examples above we are defining only 1 getter in the request, but this action occurs several times in both the request and the response, greatly reducing the number of requests per second that Express can serve. With this change, Express will have the same performance as Koa and Fastify. Basically, I know because I have already tested it by basically rewriting the request/response with the same functions currently present. I did not send a PR for the change because I was waiting for version 5 to be officially available to check if this point was changed. However, checking the version code I found that it is apparently the same as version 4.

I hope I have helped improve the project, and if you want my help to change the code I am available, see you later =)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions