How to increase the PHP App start performance by up to 60% with Opcode Preloading

How to increase the PHP App start performance by up to 60% with Opcode Preloading

In the early days, PHP parsed and compiled any file used to serve a request. The parsed/compiled result, which is called opcode, was not reused for further requests.

With PHP 7.4, support for preloading was added, a feature that improves the start performance up to 60% by preloading the most commonly used files from a framework.

In a nutshell, this is how it works:

  • For preloading files, a custom PHP script is needed
  • The preloading script is executed once after PHP-FPM start/restart
  • All preloaded files are stored and available in memory for all requests
  • Changes made to preloaded files won't have any effect until PHP-FPM restart

Performance

Using the preloading script from Symfony, I got about 50% speed-up on the first request and up to 60% on a Magento 2 installation.

However, real-world gains depend on the ratio between the bootstrap overhead and the runtime of the code.

Small scripts or microservices with a very lightweight bootstrap will probably not benefit that much.

CLP Opcache Preloader

Some frameworks like Symfony already generate preload scripts that can be used.

Other PHP Apps and frameworks like Laravel, Drupal, Magento, or WordPress don't yet provide a generated preload script that can be used.

We have developed and open-sourced the CLP Opcache Preloader to address this issue, which can be easily modified for your PHP App.

At the end of the preload script, you can add and ignore files:

<?php
$clpPreloader = new ClpPreloader();
$clpPreloader->setDebug(false);
$clpPreloader->paths(realpath(__DIR__ . '/src'));
$clpPreloader->paths(realpath(__DIR__ . '/vendor'));
$clpPreloader->ignore(realpath(__DIR__ . '/vendor/twig/twig'));
$clpPreloader->preload();

Configuration

To enable opcache preloading, you must tell PHP where this file is stored in its php.ini configuration file.

  1. Open the php.ini of your PHP Version:
sudo nano /etc/php/7.4/fpm/php.ini
  1. Search for opcache.preload_user and opcache.preload and adjust the values:
opcache.preload_user=clp
opcache.preload=/home/cloudpanel/htdocs/www.domain.com/preload.php

If you are using a custom PHP-FPM Pool, change the opcache.preload_user.

If you use the default configuration, use www-data as opcache.preload_user.

  1. Restart the PHP-FPM service to apply the changes:
sudo systemctl restart php7.4-fpm
If the PHP-FPM service doesn't start it's because of a problem with the preload script. In that case you, need to check the php error log for detailed information.

Does it work?

After the configuration, we want to know if the opcache preloading is working as expected; for that we can check the output of the php function opcache_get_status().

  1. Create a test script like t.php and put it in your document root:
nano /home/cloudpanel/htdocs/www.domain.com/public/t.php
  1. Add the following lines of php code:
<?php
echo '<pre>';
print_r(opcache_get_status());
echo '</pre>';
  1. Restart the PHP-FPM service to clear and preload the opcode cache:
sudo systemctl restart php7.4-fpm
  1. Open the test script in your browser to see the opcache statistics.

The value of num_cached_scripts is worth checking. The higher this number, the more scripts have been preloaded.

Array
(
    [opcache_enabled] => 1
    [cache_full] => 
    [restart_pending] => 
    [restart_in_progress] => 
    [memory_usage] => Array
        (
            [used_memory] => 28023040
            [free_memory] => 777282624
            [wasted_memory] => 704
            [current_wasted_percentage] => 8.7420145670573E-5
        )

    [interned_strings_usage] => Array
        (
            [buffer_size] => 6291008
            [used_memory] => 4359696
            [free_memory] => 1931312
            [number_of_strings] => 56348
        )

    [opcache_statistics] => Array
        (
            [num_cached_scripts] => 1000
            [num_cached_keys] => 1000
            [max_cached_keys] => 130987
            [hits] => 128
            [start_time] => 1611310634
            [last_restart_time] => 0
            [oom_restarts] => 0
            [hash_restarts] => 0
            [manual_restarts] => 0
            [misses] => 25
            [blacklist_misses] => 0
            [blacklist_miss_ratio] => 0
            [opcache_hit_rate] => 83.660130718954
        )

    [preload_statistics] => Array
        (
            [memory_consumption] => 12108632
            [functions] => Array
                (
                    [0] => Symfony\Component\Routing\{closure}
                    [1] => Symfony\Component\Routing\{closure}
                    [2] => Symfony\Bundle\WebProfilerBundle\Csp\{closure}
                    [3] => Symfony\Component\VarDumper\Dumper\ContextProvider\{closure}
                    [4] => Symfony\Component\VarDumper\Cloner\{closure}
                    [5] => Symfony\Component\Validator\{closure}
                    [6] => Symfony\Component\Translation\{closure}
                    [7] => Symfony\Component\String\{closure}
                    [8] => Symfony\Component\String\{closure}
                    [9] => Symfony\Component\String\{closure}
                    [10] => Symfony\Component\String\{closure}
                    [11] => Symfony\Component\String\{closure}
                    [12] => Symfony\Component\String\{closure}
                    [13] => Symfony\Component\String\{closure}
                    [14] => Symfony\Component\String\{closure}
                    [15] => Symfony\Component\String\{closure}
                    [16] => Symfony\Component\String\{closure}
                    [17] => Symfony\Component\String\{closure}
                    [18] => Symfony\Component\String\Slugger\{closure}
                    [19] => Symfony\Component\HttpKernel\EventListener\{closure}
                    [20] => Symfony\Component\HttpFoundation\Session\Storage\{closure}
                    [21] => Sensio\Bundle\FrameworkExtraBundle\EventListener\{closure}
                    [22] => Sensio\Bundle\FrameworkExtraBundle\Security\{closure}
                    [23] => Sensio\Bundle\FrameworkExtraBundle\Security\{closure}
                    [24] => Sensio\Bundle\FrameworkExtraBundle\EventListener\{closure}
                    [25] => Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\{closure}
                    [26] => Sensio\Bundle\FrameworkExtraBundle\EventListener\{closure}
                    [27] => Symfony\Bundle\SecurityBundle\Security\{closure}
                    ..........

Limitations

Unfortunately, it's only possible to define one preload script in the php.ini, which can cause conflicts if you are running multiple PHP Apps on the same server with the same PHP Version.

I thought it would be possible to define a opcache.preload script on the PHP-FPM Pool level, but it isn't possible yet.

For multiple PHP Apps, we could just (theoretically) create a new PHP-FPM Pool with its own user and opcache.preload script defined.

Conclusion

The new feature, opcache preloading, which has been introduced with PHP 7.4, can speed up the start performance by up to 60%. It's a great way to improve the startup performance, especially interesting if you are running an Auto Scaling setup where you scale the web servers or containers on demand.

However, it's a new feature and needs intensive testing before using it in production. I recommend enabling opcache preloading during the development and testing phase.

Stefan Wieczorek
Stefan Wieczorek
Founder & CTO

He has over 20 years of experience as a PHP Developer and Linux System Administration. He likes to develop solutions to make complicated things easy to use.

You can find him in the CloudPanel Discord Channel.


Deploy CloudPanel For Free! Get Started For Free!