Home / Blog /

Increase Laravel performance with preloading (PHP 7.4+)

3 years ago

Increase Laravel performance with preloading (PHP 7.4+)

The release of PHP 7.4 contained some features focused on performance. One of these features is called preloading. Preloading makes use of a preload script, that will execute on server start-up. This preload script loads cached PHP files into memory. In this blog post, I would like to share an example I discovered in the Front Line PHP book, created by Brent Roose of Spatie. 

👀 460 views

Preloading works with OPCache. This makes it possible for the server to easily interpret the cached code. Preloading allows the compilation of source files with related classes, traits and interfaces. In order to make use of preloading for Laravel, the following script can be used, create the following file in the root directory: preload.php.

<?php

class Preloader
{
    private array $ignores = [];
    private static int $count = 0;
    private array $paths;
    private array $fileMap;

    public function __construct(string ...$paths)
    {
        $this->paths = $paths;
        // We'll use composer's classmap
        // to easily find which classes to autoload,
        // based on their filename
        $classMap = require __DIR__ .
            '/vendor/composer/autoload_classmap.php';

        // We flip the array, so that file paths are the array keys
        // With it, we can search the corresponding class by its path
        $this->fileMap = array_flip($classMap);
    }

    public function paths(string ...$paths): Preloader
    {
        $this->paths = array_merge(
            $this->paths,
            $paths
        );
        return $this;
    }

    public function ignore(string ...$names): Preloader
    {
        $this->ignores = array_merge(
            $this->ignores,
            $names
        );
        return $this;
    }

    public function load(): void
    {
        // We'll loop over all registered paths
        // and load them one by one
        foreach ($this->paths as $path) {
            $this->loadPath(rtrim($path, '/'));
        }
        $count = self::$count;
        echo "[Preloader] Preloaded {$count} classes" . PHP_EOL;
    }

    private function loadPath(string $path): void
    {
        // If the current path is a directory,
        // we'll load all files in it
        if (is_dir($path)) {
            $this->loadDir($path);
            return;
        }

        // Otherwise we'll just load this one file
        $this->loadFile($path);
    }

    private function loadDir(string $path): void
    {
        $handle = opendir($path);

        // We'll loop over all files and directories
        // in the current path,
        // and load them one by one
        while ($file = readdir($handle)) {
            if (in_array($file, ['.', '..'])) {
                continue;
            }
            $this->loadPath("{$path}/{$file}");
        }

        closedir($handle);
    }

    private function loadFile(string $path): void
    {
        // We resolve the classname from composer's autoload mapping
        $class = $this->fileMap[$path] ?? null;

        // And use it to make sure the class shouldn't be ignored
        if ($this->shouldIgnore($class)) {
            return;
        }

        // Finally we require the path,
        // causing all its dependencies to be loaded as well
        opcache_compile_file($path);
        self::$count++;
        echo "[Preloader] Preloaded `{$class}`" . PHP_EOL;
    }

    private function shouldIgnore(?string $name): bool
    {
        if ($name === null) {
            return true;
        }
        foreach ($this->ignores as $ignore) {
            if (strpos($name, $ignore) === 0) {
                return true;
            }
        }
        return false;
    }
}

(new Preloader())
    ->paths(__DIR__ . '/vendor/laravel')
    ->ignore(
        \Illuminate\Filesystem\Cache::class,
        \Illuminate\Log\LogManager::class,
        \Illuminate\Http\Testing\File::class,
        \Illuminate\Http\UploadedFile::class,
        \Illuminate\Support\Carbon::class,
    )
    ->load();

Before this preloading script is used, make sure OPCache is enabled. Additionally, the preload location of the script needs to be defined in the php.ini (location might be different on your server).

opcache.preload="/home/youruser/yourwebsite.com/preload.php"

Make sure to restart the server (or PHP FPM, should be sufficient) using sudo -S service php7.4-fpm reload. Make sure to use the correct service name. Consider adding this to an automatic deployment script, to not forget the reloading of FPM.