Introduction
When you launch your website into production, it’s crucial to ensure that it delivers optimum performance. Unfortunately, many PHP sites fail to reach their full potential in terms of speed and responsiveness. An unoptimized PHP application can result in slow loading times, a poor user experience and inefficient use of server resources. In this article, we’ll explore the risks associated with using an unoptimized PHP site, and the importance of adopting optimization practices to deliver an exceptional user experience.
An unoptimized PHP site can cause significant delays in page loading, which can frustrate visitors and reduce engagement. In a world where users are accustomed to fast, fluid navigation, every millisecond counts. If your site is slow to load, visitors may turn away and look for faster, more efficient alternatives.
What’s more, an unoptimized PHP site can have a negative impact on server resource utilization. Slow response times mean that the server is able to handle fewer requests per unit of time, reducing its ability to handle high traffic efficiently. As a result, your site may experience performance problems as the number of visitors increases.
What’s more, unoptimized PHP code can lead to excessive use of system resources such as memory and CPU. Complex, inefficient operations, poorly designed SQL queries or poorly optimized loops can slow down PHP code execution and lead to over-utilization of resources. This can translate into additional costs if you need to upgrade your infrastructure to cope with this overload.
So it’s crucial to pay particular attention to performance optimization from the earliest stages of your PHP application’s development. By adopting sound optimization practices, using caching tools such as the Opcache extension and performing regular code updates, you can significantly improve your site’s performance and deliver an outstanding user experience.
In the following chapters, we’ll explore in detail various techniques and approaches for optimizing the performance of your PHP application. We’ll look at choosing a web server, PHP execution mode, using Composer, configuring Opcache, updating code and conclude with some practical advice. By following these recommendations, you’ll be able to maximize the performance of your PHP site, delivering a smooth, responsive and satisfying user experience.
Before delivering the valuable tips, it’s necessary to lift the hood to understand how Web servers work with PHP sites in general, before we can dive deeper into the inner workings of the PHP engine.
When an HTTP request arrives on a Web server and needs to be processed by PHP, it follows a specific workflow. Understanding this workflow is essential for optimizing the performance of your PHP application. Here’s a detailed description of the key stages:
- 1. Request received by Web server
- 2. Static request processing
- 3. Passing the request to the PHP engine
- 4. PHP processing
- 5. Response generation
- 6. Sending response to Web server
- 7. Receipt of response by customer
This operating mode is common to all Web servers:
- When a user sends an HTTP request to your Web server, the server receives it first. The Web server can be Apache, Nginx, or any other PHP-compatible server.
- The web server checks whether the request is for a static file, such as an image, CSS or JavaScript file. If so, the Web server can send the file directly back to the client without involving PHP. This saves time and frees up resources for dynamic requests.
- If the request requires processing by PHP, the web server forwards the request to the appropriate PHP engine. In the case of PHP-FPM (PHP FastCGI Process Manager), the Web server communicates with the PHP-FPM process to execute the PHP code.
- The PHP-FPM engine receives the request and executes the associated PHP code. This can include retrieving data from the database, processing forms, accessing files, or any other application-specific operation.
- Once the PHP code is executed, it generates a response to the request. This can be a complete HTML page, JSON, XML or any other type of data to be returned to the client.
- The PHP-FPM engine sends the generated response to the web server, which is responsible for returning it to the client. The Web server can also perform further processing, such as compressing the response or adding additional headers.
- Finally, the client (the Web browser) receives the response from the server and processes the data received. This may include rendering the page, executing JavaScript scripts or other client-side processing.
It’s important to note that each step in this workflow can be optimized to improve the overall performance of the PHP application. For example, caching static requests, using a lightweight, high-performance web server, optimizing PHP code, using caching tools such as Opcache, or optimizing SQL queries can all help to reduce response times and deliver a more responsive user experience.
Choosing the right Web server
When developing a PHP application, the choice of Web server is a crucial decision. The web server plays an essential role in handling HTTP requests and in the overall performance of your application. There are several popular options, each with its own features and benefits. Let’s take a closer look at some of the Web servers most commonly used with PHP.
Apache HTTP Server
Apache is one of the oldest and most widely used Web servers. It is appreciated for its flexibility and compatibility with many operating systems. Apache supports PHP via the mod_php module, which enables direct integration between the server and the PHP engine. This facilitates the deployment of PHP scripts and offers a high degree of compatibility with existing applications. However, Apache can be relatively heavy in terms of resource consumption, which can affect performance in some cases.
Apache has several MPMs (Multi-Processing Module) and we need to know which one is best suited to our needs:
- MPM prefork
- A management method from another age, this is the worst management method in terms of performance. There is only one parent Apache process, which creates a child process on the fly when a request comes in, before killing this child process when it has finished its work. As soon as the server has to handle more requests than it has CPUs for, it will run amok, saturate, and the CPUs will be stuck at 100%, spending more energy creating and deleting processes than serving resources. In other words, it's easy to DDOS an Apache server using this management mode.
- MPM worker
- The main aim of MPM worker is to provide better resource management using a multi-threaded model. Unlike MPM prefork, MPM worker uses a set of threads to manage connections and requests. This saves system resources by reducing the overhead of creating and managing individual processes. MPM worker is particularly effective in situations where the server has to handle a large number of simultaneous connections.
- MPM event
- MPM event is a variant of MPM worker, introduced with Apache 2.4. It further improves performance by introducing more efficient management of persistent requests. With MPM event, persistent connections are handled separately in a dedicated thread, while other connections are managed as with MPM worker, in a thread pool. This maximizes server resource utilization and improves request response capacity.
It should be noted that the choice of MPM depends on a number of factors, including the type of application, the number of simultaneous connections expected and the resources available. MPM prefork is often used with PHP in its mod_php form, as it offers direct compatibility with this module without the need for configuration. However, MPM worker and MPM event offer much better performance in high-traffic environments. We strongly recommend that you do not use MPM prefork outside your development workstation, and even less so in production.
Nginx
Nginx is a lightweight, high-performance and increasingly popular Web server. It is renowned for its ability to efficiently handle large numbers of simultaneous connections with minimal resource consumption. Nginx can be used with PHP using PHP-FPM (PHP FastCGI Process Manager) to handle PHP requests. This combination delivers good performance and stability, making it a wise choice for high-traffic applications.
Unlike Apache, Nginx uses an asynchronous, event-driven processing model, enabling it to handle large numbers of simultaneous connections efficiently and with low resource consumption. Let’s take a look at process management in Nginx.
- Nginx master process
- When Nginx starts up, it creates a master process. The role of the master process is to coordinate the other processes and manage administrative tasks. The master process reads the configuration, opens listening sockets and supervises work processes.
- Nginx worker processes
- The master process also creates one or more "worker processes" which do the actual work of processing requests. Each worker process is independent and can handle multiple simultaneous connections using non-blocking I/O mechanisms and events.
- Nginx cache manager process
- In addition to the main and work processes, Nginx can also create an additional process called the "cache manager process". This process is responsible for managing the static content cache. It monitors cache usage, deletes expired entries and frees space when necessary.
Nginx’s thread-based architecture enables it to handle a large number of simultaneous connections with a relatively small memory footprint. Unlike Apache, which uses a prefork or multi-threaded process model, Nginx follows a non-blocking event-driven model that saves system resources and improves overall performance.
Nginx is capable of serving several thousand static resources per second, making it a Web server of choice, far ahead of Apache, when performance is a priority. However, it’s important to remember that I’m only talking about static files at the moment. When it comes to serving PHP pages, things are quite different and require a separate chapter, like the one that follows!
Choose the right PHP execution mode
There are 2 ways to run a PHP site. The first is simple, easy to set up, practical for developers, but about as powerful as driving a car with square wheels, and the other is more complex, but made specifically for maximum performance in a production environment.
mod_php
Let’s start with mod_php. In this mode, PHP is integrated directly into the Apache server as a module. This means that each Apache process has its own embedded PHP interpreter, enabling tight integration between the two. However, this can lead to memory overload if you have many Apache processes running simultaneously, as each process must load and maintain its own PHP interpreter. Indeed, to use the PHP module, Apache must use MPM prefork, which as we saw earlier, is the worst possible option in terms of performance.
Processing a request with mod_php results in the following workflow:
- Request
- Apache master
- + Apache child
- + PHP
- - PHP
- - Apache child
- Response
- As seen with MPM prefork, the Apache master process creates a child process to handle each request.
- The child process realizes that this is a request for a PHP file, and so creates a PHP process to handle this request.
- The PHP process processes the request and returns the response to the child Apache process.
- The child Apache process kills the PHP process and returns the response to the master Apache process.
- The Apache master process kills the Apache child process and returns the response to the user.
To help you visualize the absurdity of the situation, it’s like going to a restaurant:
- The host pulls out a magic wand and summons a table as well as a waiter, who stands behind you and only processes your order.
- That when you wanted to place an order, the waiter would also pull out a magic wand to summon an entire kitchen, complete with cooks, just for your order.
- That when the cooks finished preparing your plate, the waiter would pull a .44 magnum from his pocket and coldly shoot the cooks, before making the kitchen disappear with his magic wand.
- And finally, when you’d finished eating and exited the restaurant, the host would in turn shoot the waiter he had summoned just for you.
- And this, individually, for each customer who came to eat in this restaurant.
So while I admit that this would be the most METAL 🤘🏻 restaurant I’ve ever been to, it would also be the least efficient restaurant in the world.
Moral of the story, not killing your employees is a good thing. And that’s exactly what PHP-FPM does!
PHP-FPM
PHP-FPM is a FastCGI process management solution for PHP. In this mode, PHP is run as an independent process, separate from the Web server. PHP-FPM manages PHP processes in a highly efficient way, in the same way as Apache MPM worker, MPM event or Nginx, PHP-FPM creates in advance a pool of child processes to handle requests. This avoids the need to create and kill child processes over and over again, and concentrates server resources on generating pages. This optimizes the use of system resources and improves overall performance. What’s more, PHP-FPM offers great flexibility, as it can be used with different Web servers such as Nginx, Apache (via MPM event or MPM worker). It also makes it easy to update PHP without having to restart the Web server. PHP-FPM is particularly recommended for high-traffic environments where scalability is a key factor.
The second advantage of using PHP-FPM (apart from performance) is that you can install several of them, to support several versions of PHP in parallel, which is very practical when you’re managing a server serving several sites, each running a different version of PHP (even though it’s really important to update your code to the latest version of PHP).
Now that we’ve installed PHP-FPM, it’s time to configure it properly. The default values in the file /etc/php/<your_version>/fpm/pool.d/www.conf
are fine for normal use, but are not really suitable for a site with very high traffic levels.
The parameters we’re interested in are :
Paramètre | Description | Options |
---|---|---|
pm | How PHP-FPM manages the number of its child processes. | static : The number doesn't change. dynamic : The number fluctuates according to the load, but within a defined limit. ondemand : Children are created on demand (like the mpm_prefork mode for Apache). |
pm.max_children | Maximum number of child processes. | pm=static: This is the number of child processes that will be used. pm=dynamic: The number of child processes can be increased up to this limit. pm=ondemand: The number of child processes can be increased up to this limit. |
pm.start_servers | Number of child processes to be created at PHP-FPM startup. | Only used with pm=dynamic. |
pm.min_spare_servers | Minimum number of childcare processes even when there's not enough traffic for them to work. | Only used with pm=dynamic. |
pm.max_spare_servers | Maximum number of childcare processes when there's not enough traffic for them to work. | Only used with pm=dynamic. |
pm.process_idle_timeout | Nombre de secondes d'inactivité après lesquels un processus enfant est tué. | Utilisé uniquement avec pm=ondemand. |
pm.max_requests | Number of requests a child process can handle before being restarted. | Used with all modes. |
For high-traffic sites, these parameters are not suitable. It’s best to switch PHP-FPM to static mode, get out your calculator and configure the parameters as follows:
- pm=static
- No more resources are spent creating or killing child processes. Even if traffic is down, we continue to maintain the same number of child processes consuming server resources.
- pm.max_children=(take out your calculator)
This value is very difficult to calculate, and an error in estimation can end up saturating your server. First, you need to estimate the average RAM consumption of your PHP application (e.g. 100 MB). Then you take the total RAM your server has (e.g. 12 GB), from which you subtract a margin for the OS and other applications present on the server (e.g. -3 GB). Take the remaining total and divide it by the RAM consumption of your PHP application found in the first step.
In my example, with a PHP app that consumes 100 MB on a server with 12 GB of RAM and for which we keep 3 GB of RAM in reserve for other applications, this gives us (12 - 3) / 0.1 = 90 child processes.
If you're too optimistic about your PHP application's power consumption, you'll end up completely saturating your server's RAM. And if, on the other hand, you imagine your application to be too heavy in relation to reality, you won't be able to take full advantage of the server's performance.
- pm.max_requests=1000
- For production use, you can allow at least 1,000 requests per child process, as your application code should be stable and optimized enough not to have too many memory leaks. If your application's code has been lovingly crafted, you can raise this limit to 5,000, or even 10,000 if you don't notice any long-term memory problems.
Configuring PHP-FPM is not extremely difficult, but requires either a very good knowledge of RAM consumption, both of your PHP site and of other applications installed on the server, or regular monitoring in order to optimize the configuration little by little.
It’s also worth noting that in terms of performance, using Apache or Nginx makes no difference to PHP-FPM, since both simply act as a pass-through between the user request and PHP-FPM (but Nginx remains far ahead for all other static requests).
Composer
Today, Composer is an essential tool for managing dependencies in PHP applications. It acts as a central entry point for listing all classes, locating their physical locations on disk and loading them into memory when necessary.
Fortunately, Composer already does this automatically. The days when we had to use require_once
instructions in all our PHP files are long gone.
But did you know that Composer also offers options for optimizing these loads? However, these options should not be used during development, as they prevent the loading of new dependencies installed at a later date (a new global installation will have to be launched via Composer).
By default, when you import a class, Composer checks in the background whether the corresponding file exists on disk, then loads it. This means there are two disk accesses per file. With today’s sites using hundreds of libraries, totalling several thousand classes, these checks have a significant cost in terms of CPU and disk access. However, if all libraries comply with PSR standards for class and namespace naming, it is possible to optimize Composer so that it generates a single, large table listing all classes and their locations, without checking whether they actually exist on the hard disk. This optimization allows you to go straight to the file loading stage, without spending resources checking for their existence (unless you have a hard disk that mysteriously makes files disappear at random, the files should always be present).
When installing your PHP site on the server, replace the composer install
command with composer install -o -a
.
If you’ve already installed your dependencies, you can run the command composer dump-autoload -o -a
to achieve the same result without reinstalling all your dependencies.
From now on, all classes imported by Composer will be loaded more quickly, thanks to the fact that their namespace provides sufficient information to locate their file physically on the hard disk. This halves the number of operations required to load them.
Please note that this optimization works ONLY for sites that comply with PSR standards. As Drupal physically installs its core files in a different location from that indicated by their namespace, this optimization cannot be used in this case.
Configuring Opcache
Opcache is PHP’s internal caching system designed to speed up the loading and execution of PHP files. It is installed by default with recent versions of PHP, but its default configuration is very lightweight and not suitable for complex, large applications such as sites based on Symfony, Laravel, Drupal and other major PHP frameworks.
To understand how Opcache works, it’s important to understand how PHP actually works. Indeed, when you deliver PHP source code to a server, it’s not exactly what’s executed when these files are called.
- Code source
- Parsing
- Bytecode
- Execution
- Response
- When the PHP engine is tasked with executing the contents of a PHP file, it loads it and parses it to generate a more machine-readable structure. This parsing process generates an abstract logic tree called the Abstract Syntax Tree (AST). Parsing is extremely costly in terms of CPU computation, especially as each new version of PHP further optimizes performance at runtime, which means greater complexity and increased computation time when generating the AST.
- The abstract logic tree is then transformed into bytecode (opcode), which is an optimized binary representation ready for execution by the PHP engine.
- The PHP engine imports the bytecode and executes it using direct instructions for the CPU and the PHP virtual machine (yes, like Java, but hidden in its bowels and nobody pays any attention to it). It executes all instructions until the response is returned to the process that requested the PHP file to be loaded…
These steps are repeated every time a file is loaded, even if it’s the same file that’s called up each time.
It’s easy to see that there’s a performance problem. Since the source file doesn’t change with each call, why parse and generate bytecode every time? This is precisely where Opcache comes in.
Opcache interposes itself between the reading of a file’s source code by the PHP engine and the generation of bytecode, by caching the generated bytecode. In this way, it bypasses the resource-intensive part of the process, which is useless because the files have not changed between two requests.
So, from the second call onwards, the PHP workflow boils down to :
- Execution
- Response
You can then clearly see the performance gains offered by PHP 😁.
However, as mentioned above, the default Opcache configuration is not suitable for large sites or production environments. Let’s take a look at how to correct this.
Cache
In your installation’s php.ini
file, in the “Opcache” section near the end of the file, you will find the following configuration keys:
opcache.enable
This key determines whether Opcache should be enabled for Web requests. We recommend enabling it by setting the value to
1
.
opcache.enable_cli
This key enables Opcache for PHP CLI (Command Line Interface) commands. If you’re using the same
php.ini
file for both Web requests and CLI commands, it’s best to disable Opcache in CLI by setting the value to0
.This ensures that the version of code present on disk is used during deployments, even if this may be slower, rather than bypassing this control and loading an earlier version into memory.
opcache.memory_consumption
This key determines the maximum amount of cache you allow to occupy memory. We recommend that you set the maximum possible value, i.e.
128
MB. Since this cache is shared between the various PHP-FPM processes, its impact on RAM is minimal.
opcache.interned_strings_buffer
For this value, you need to understand how PHP handles hard-coded text values. If I give you the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php namespace App\Entity; class User { private string $name = "John"; public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } }
If I tell you that the value
John
is a string cached by PHP, I shouldn’t surprise you 😉. On the other hand, if I tell you that PHP has also extractedApp\Entity
,User
,$name
,getName
andsetName
as a string to be cached, that’s already a little more surprising, and at the same time logical! That’s just as many text strings as you could call up in code. And so Opcache extracts all these strings, whether they’re real text or namespace, class name, variable name or function name, and stores them in its cache!In fact, in addition to real text values, Opcache extracts and caches strings corresponding to namespaces, class names, variable names and function names. In the case of large PHP applications with many vendors, this cache can fill up quickly. We therefore recommend setting the maximum value, currently
32
MB.
opcache.max_accelerated_files
This key defines the maximum number of PHP files for which Opcache keeps their bytecode representation in memory. In large projects with a large number of PHP files (including vendors), the default limit is quickly reached. We recommend setting the maximum limit to
100000
files.
opcache.max_wasted_percentage
This key specifies the maximum percentage of wasted cache (memory leak) allowed in memory before PHP restarts to forcibly flush its cache. If you have sufficient RAM on your server, we recommend setting this value to
15
%. This strikes a balance between maintaining performance at the expense of RAM consumption and the time spent restarting PHP processes rather than responding to requests.
opcache.validate_timestamps
This is a subtle point. By default, Opcache stores the time at which the bytecode of a PHP file has been cached, so that it doesn’t check the file on every call, but rather after a few seconds (2 seconds by default).
As much as this is very kind of PHP and developer-friendly, it wastes time and resources in production, as PHP files are generally not modified frequently and never by themselves.
To maximize performance, it is recommended to set this value to
0
, which allows PHP to keep files permanently cached without checking whether they have been modified on disk.On the other hand, when a new version is deployed on the server, it will be necessary to restart PHP-FPM to completely empty the cache and load the new version of the files.
With this new configuration, your PHP site should already benefit from a performance improvement of around 15% to 20%, which is significant for the optimization of your application 😁.
Preload
To further optimize the performance of your PHP site, we’re going to exploit the Preload feature introduced with PHP 7.4.
The concept of Preload is similar to what we’ve already implemented with Opcache, where we configured Opcache to permanently keep the version cached without ever checking the files on disk. However, Preload goes one step further. Instead of storing the bytecode in RAM, it stores the compiled code itself and in PHP’s core. This means that preloaded functions are not part of your code, but become native PHP elements available globally. The advantage is a significant performance gain for classes loaded via Preload. However, the disadvantage is that, as with the Opcache configuration in intensive mode, you’ll need to restart PHP-FPM after deploying a new version.
It’s important to note that the ability to use Preload depends on the solution on which your site is built. To be able to “preload” classes, you need to provide PHP with a special file to run when the PHP-FPM service is started. This file will store the most frequently used classes.
For example, if your site is based on Symfony, Preload is natively supported and the file to be supplied to PHP is app/config/preload.php
. On the other hand, for Drupal, Preload is not natively supported, but there is a module that allows you to do so.
Once you have your PHP Preload file, simply modify your php.ini
and specify the ABSOLUTE PATH of your Preload file as the value of the opcache.preload
configuration. This step enables PHP to load and store preloaded classes, resulting in significant performance improvements.
Don’t forget that each time you deploy a new version of your site, you’ll need to restart PHP-FPM for the preload changes to take effect and for the new classes to be loaded.
Update your code
Congratulations! Now that you’ve installed Nginx, migrated to PHP-FPM, configured it to make the most of your production server’s resources, optimized Opcache and set up Preload, you’ve managed to cut the execution times needed to generate your visitors’ pages in half, at the very least! 🎉
However, I’ve got some bad news for you. You’ve reached the top of the mountain, you’re at the summit, and there’s nothing left to climb to get any higher. You’ve optimized your server for maximum performance while scrupulously respecting your source code and its instructions.
If, despite all these optimizations, your pages are still taking more than 100 ms to generate, then it’s time to roll up your sleeves and do some serious code refactoring.
Here are a few avenues to explore:
- PHP update: Make sure you’re using the latest version of PHP. Each new release brings further improvements in terms of performance and optimization.
- Optimize SQL queries: Check your SQL queries. Do you have primary keys, foreign keys and indexes on relevant columns? Optimize your queries to minimize database access times.
- Number of SQL queries: Are you avoiding 300 SQL queries per page load? Prefer to retrieve a set of data and then loop over it, rather than making SQL calls within a loop.
- Reduce complex operations: If you perform complex operations on every page load, consider moving them to the background or caching the results of these operations. Reduce intensive tasks as much as possible to speed up page generation.
- Use native functions: Use PHP’s native functions, such as
array_map()
,array_filter()
,array_reduce()
, etc., to perform operations on arrays. These functions are generally optimized and offer good performance. - Replace annotations with attributes : The attribute system added since PHP 8 makes it possible to replace annotations grafted into comments and parsed at runtime, with real objects native to PHP, and so they too are optimized and compiled by PHP, unlike comments.
- Use of a Redis server: Consider setting up a Redis server for application caching. Redis is a fast in-memory storage solution that can significantly improve your application’s performance by temporarily storing frequently used data.
- Microservices approach: If none of the above solutions solve your problem, it may be time to consider a microservices approach. Divide your monolithic application into several small, specialized, lightweight, high-performance applications. This will spread the load and improve your system’s overall scalability and performance.
Conclusion
Installing PHP on a development machine or server is relatively straightforward, making it easy to set up a website quickly. This ease of use is one of PHP’s major strengths, but it also represents one of its main weaknesses, as all default configurations are designed to facilitate the work of developers and are not at all optimized for a production environment.
However, it is entirely possible to get to grips with the production server in less than an hour and optimize every detail, right down to the last cache byte. These optimizations can reduce PHP’s execution time by 50% to 75%, enabling more pages to be served faster, saving precious time. 🎉