In the ever-evolving world of software engineering, performance is a major factor that impacts your choice of technology. Laravel Octane and Node.js are two of the popular technologies used by large corporation that provide high-performance solutions for building web applications.
In this article, we will build different HTTP servers using Swoole, Open Swoole, RoadRunner, Node.js, AdonisJS, and Laravel. We will then benchmark and compare their load test analysis to gain better insights into their performance. Take a look at all of our source code for this post on GitHub.
Jump ahead:
Node.js is a JavaScript runtime environment based on Google Chrome’s V8 engine designed to execute JavaScript code outside a web browser. It operates in a single-threaded event loop using non-blocking I/O, which aims to optimize throughput and scalability in web apps with many I/O operations, real-time features, and so much more.
Some Node.js use cases include:
Octane is a Laravel package introduced in Laravel 8 that aims to enhance the performance of Laravel applications. Octane achieves this by using one of the following extensions:
At the time of writing, Octane is designed specifically for the Laravel framework only, but the extensions can also be used directly in other PHP applications.
Some use cases of Octane include:
Features | Node.js | Octane |
---|---|---|
Async support | Node.js has native support for asynchronous operations | Octane leverages its extensions for asynchronous operations |
gRPC support | Node.js requires an external package to implement gRPC | Octane has built-in support for gRPC through Open Swoole |
Web sockets | Node.js has built-in support for socket programming | Octane has built-in support for sockets using Swoole |
Event driven | Node.js uses an event loop to handle concurrency | Octane handles concurrent operations using PHP coroutines |
TCP & UDP | Node.js has built-in support for TCP and UDP | Octane also supports TCP and UDP through Swoole and RoadRunner |
Comparing a full-fledged web framework like Laravel against Node.js may not be the most ideal approach for a performance analysis, but we will endeavor to focus on benchmarking each language’s runtime (PHP and Node.js) in their raw states, without external libraries, to derive meaningful insights. By doing so, we aim to gain a deeper understanding of their fundamental performance characteristics.
Furthermore, we will explore the performance analysis between Laravel Octane and AdonisJS, both of which are equally robust frameworks, in order to better grasp their individual strengths and weaknesses.
To evaluate the performance, we will focus on the following aspects:
We can install Open Swoole using PECL or Docker. Before installing, ensure that PHP is already installed, preferably version 7.4 or higher. To proceed with the installation, execute the following commands:
pecl install openswoole
You might encounter the following error, in case it is not already installed on your system:
To resolve the error, you might need to install the OpenSSL package with the following command:
pecl install openssl
If everything went smoothly, you should see a similar screenshot as below. The installation process involves building the extension from the source code and then adding it to the php.ini
file:
Let’s create a project folder named laravel-vs-octane
to house all our source code and project files. This will help organize and manage the development process effectively. Here’s a sample of our folder structure.
laravel-vs-octane / adonisjs / loadtest.yml ... laravel-octane / loadtest.octane.yml ... nodejs / loadtest.nodejs.yml ... openswoole / loadtest.open-swoole.yml ... roadrunner / loadtest.roadrunner.yml ... swoole/ loadtest.swoole.yml ...
To enhance organization, let’s create a folder named openswoole
inside the parent folder laravel-vs-octane
. Within the openswoole
folder, add a PHP file to group each server alongside its load test configuration. Now, let’s add the following code to establish our Open Swoole HTTP server:
<?php $server = new OpenSwoole\HTTP\Server("127.0.0.1", 9000); $server->on("start", function (OpenSwoole\Http\Server $server) { echo "OpenSwoole http server is started at http://127.0.0.1:9000\n"; }); $server->on("request", function (OpenSwoole\Http\Request $request, OpenSwoole\Http\Response $response) { $response->header("Content-Type", "text/plain"); $response->end("Hello World\n"); }); $server->start();
Swoole installation is similar to Open Swoole, as Open Swoole is a fork of Swoole. However, there is one difference, which is the installation command:
pecl install swoole
To maintain a similar approach as we took for the openswoole
folder, let’s create another folder named swoole
inside the laravel-vs-octane
parent folder. This folder will contain a PHP file to set up our Swoole HTTP server:
$http = new Swoole\Http\Server('127.0.0.1', 9501); $http->on('start', function ($server) { echo "Swoole http server is started at http://127.0.0.1:9501\n"; }); $http->on('request', function ($request, $response) { $response->header('Content-Type', 'text/plain'); $response->end('Hello World'); }); $http->start();
To run both the Open Swoole and Swoole servers, open your CLI or terminal and execute the following commands, as depicted in the screenshot below, to start the servers:
php swoole-server.php Swoole http server is started at http://127.0.0.1:9501
Here is a preview of our work so far:
curl -i http://localhost: 9501 HTTP /1.1 200 OK Content-Type: text/plain Server: swoole-http-server Date: Wed, 26 Jul 2023 23:26:11 GMT Connection: keep-alive Content-Length: 11 Hello World
To set up our RoadRunner server, we will maintain the folder structure by creating a dedicated folder for it, as we did for both Swoole and Open Swoole. Inside this folder, we will install the required packages using Composer:
roadrunner-cli
roadrunner
roadrunner-http plugin
roadrunner binary
composer require spiral/roadrunner-cli --dev composer require spiral/roadrunner composer require spiral/roadrunner-http
This must be done after you have installed the other packages:
vendor/bin/rr get
RoadRunner is a centralized processor for PHP applications, employing various plugins such as HTTP, gRPC, jobs, monitoring, and microservices. It communicates with the Go web server using protocols like FastCGI or PSR-7 HTTP, making it a viable replacement for PHP-FPM. The configuration file can be in YAML or JSON format, named .rr.yaml
or .rr.json
.
Below is a sample of our configuration file leveraging the HTTP plugin:
version: '3' server: command: 'php app.php' relay: pipes http: address: '0.0.0.0:8000'
Here’s the PHP worker that communicates with the web server through PSR-7 HTTP:
<?php require __DIR__ . '/vendor/autoload.php'; use Nyholm\Psr7\Response; use Nyholm\Psr7\Factory\Psr17Factory; use Spiral\RoadRunner\Worker; use Spiral\RoadRunner\Http\PSR7Worker; $worker = Worker::create(); $factory = new Psr17Factory(); $psr7 = new PSR7Worker($worker, $factory, $factory, $factory); while (true) { try { $request = $psr7->waitRequest(); } catch (\Throwable $e) { $psr7->respond(new Response(400)); continue; } try { $psr7->respond(new Response(200, [], 'Hello RoadRunner!')); } catch (\Throwable $e) { $psr7->respond(new Response(500, [], 'Something Went Wrong!')); $psr7->getWorker()->error((string)$e); } }
To start the server, we use the following command:
./rr serve
If everything is setup correctly, you should see something like this:
> ./rr serve 2023-07-29T13:12:50÷0000 DEBUG server worker is allocated {"pid": 8328, "inter nal event name": "EventworkerConstruct!) 2023-07-29113:12:50+0000 DEBUG server worker is allocated nal event name"; "EventworkerConstruct"} {"pid": 8320, "inter 2023-07-2913:12:50+0000 DEBUG server worker is allocated nal event name"; "EventworkerConstruct"} {"pid": 8325, "inter [INFO] RoadRunner server started; version: 2023.2.1, buildtime: 2023-07-2717:12:33+0000 2023-07-29T13:12:50+0000 DEBUG http http server was started {"address": "0.0.0.0
We’ll organize our Node.js server in a similar way to how we did it previously. We’ll create a special folder named nodejs
and then proceed to place the server code inside of a file called server.js
.
The server contains two endpoints, in which /data
will later be integrated by Laravel Octane and AdonisJS service:
const http = require('http'); const hostname = 'localhost'; const port = 3000; const server = http.createServer((req, res) => { console.log(`[${new Date().toISOString()}] Incoming request: ${req.method} ${req.url}`); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET'); res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // Set the response header if (req.url === '/' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello nodejs!'); } else if(req.url === '/data' && req.method === 'GET') { const data = { message: 'Hello, this is your data!' }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); }else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); // Log when the server starts listening server.on('listening', () => { console.log(`[${new Date().toISOString()}] Server is listening on http://${hostname}:${port}/`); }); // Log any server errors server.on('error', (error) => { console.error(`[${new Date().toISOString()}] Server error: ${error.message}`); }); server.listen(port, hostname);
To install AdonisJS in the parent folder, please make sure you have Node.js installed. Then, open your command-line interface (CLI) or terminal and run the following command:
npm init adonis-ts-app@latest adonisjs-api
Follow the instructions provided by the command and make sure to select api
as the application type. This will create an AdonisJS project with the necessary setup for building an API.
Open the routes.ts
file and add the following code to retrieve records from our Node.js service:
Route.get('/external-service', async () => { try { const url = 'http://localhost:3000/data' const result = await axios.get(url) return result } catch (error) { return {error: `Error fetching data from the external endpoint: ${error.message}`}; } })
Then, start the server using:
node ace serve --watch
To integrate Laravel Octane into the same parent folder as your Laravel project, we’ll use Swoole as the default application server for your Laravel application:
composer create-project laravel/laravel laravel-octane
Install Laravel Octane using the following command:
composer require laravel/octane
Run this command to install Octane’s configuration file into our app:
php artisan octane:install
Follow the instructions and select 1
for Swoole:
Which application server you would like to use?: [0] roadrunner [1] swoole > 1
Open the web.php
file and add the following code to create an Octane endpoint that pulls data from our Node.js service:
Octane::route('GET', '/external-service', function () { try { $client = new GuzzleHttp\Client(); $url = 'http://localhost:3000/data'; $result = $client->get($url); $resp = $result->getBody()->getContents(); return response()->json(json_decode($resp)); } catch (Exception $e) { return response()->json(['error' => 'error: '. $e->getMessage()]); } });
We will be considering Artillery for our load test. With Artillery, we can create a load test config that can be used for all our servers. Let’s install and setup Artillery via npm:
npm install -g artillery@latest
Create a load test file named loadtest.nodejs.yml
. This config file is designed for load testing our Node.js server. You can modify the server’s URL to test or replicate this config for other servers. The configuration specifies ten virtual users or connections per seconds, resulting in approximately 600 total requests. We will measure the latency for each server during the load test:
config: target: "http://localhost:3000" # Replace this with your server's URL phases: - duration: 60 arrivalRate: 10 # Number of virtual users per second during the ramp-up phase name: "Rampp up phase" scenarios: - flow: - get: url: "/" # Replace this with the endpoint you want to load test
Here’s the overview of the load test:
Test | RoadRunner (ms) | Swoole (ms) | Open Swoole (ms) | Node.js (ms) |
---|---|---|---|---|
Average | 2.3 | 2.4 | 2.5 | 2.2 |
P99 | 9.3 | 10.3 | 9.7 | 9.7 |
To test Laravel Octane and AdonisJS, we will create another Artillery config file in their respective directories, with 20 virtual users or connections per second:
config: target: "http://localhost:8000" # Replace with :3333 for AdonisJS phases: - duration: 60 arrivalRate: 20 # Number of virtual users per second during the ramp-up phase name: "Ramp up phase" scenarios: - flow: - get: url: "/external-service" # Replace this with the endpoint you want to load test
The load test results revealed that both frameworks, AdonisJS and Laravel Octane with Swoole, were able to effectively handle an average of 20 requests per second. However, it’s worth noting that real-world performance tests could be more demanding. Interestingly, we observed a close performance similarity between them.
Test | AdonisJS (ms) | Laravel Octane with Swoole (ms) |
---|---|---|
Average | 6 | 6.4 |
P99 | 13.9 | 17.3 |
If you encounter any challenges while following this article, rest assured that all of our source code is available on GitHub.
In conclusion, the comparative analysis of Laravel Octane and Node.js often reveals that their performance impact on our application is not the primary concern. The major bottlenecks for most applications lie at the database level, such as query efficiency, optimization, indexing, and interactions with external services.
While choosing between Laravel Octane and Node.js is an important decision, optimizing database operations and external service interactions will likely have a more significant impact on overall application performance. Prioritizing these aspects will help ensure the smooth and efficient functioning of the application, regardless of the chosen server or framework.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]