Entwickler, der außergewöhnliche CRM- und Laravel-Lösungen liefert

Als erfahrener Entwickler spezialisiere ich mich auf Laravel- und Vue.js-Entwicklung, die Implementierung von Vtiger CRM sowie auf vielfältige WordPress-Projekte. Meine Arbeit zeichnet sich durch kreative, dynamische und benutzerzentrierte Weblösungen aus, die individuell an die Bedürfnisse meiner Kunden angepasst werden.

In der Welt der Software spielt die Leistung eine Schlüsselrolle. Sie beeinflusst die Benutzererfahrung, die Kundenzufriedenheit und sogar die Gesamtrentabilität des Unternehmens. Dies gilt insbesondere für PHP-Anwendungen, die für ihre Einfachheit und Flexibilität bekannt sind, aber bei nicht optimaler Nutzung mit Leistungsproblemen konfrontiert sein können.

Wir steigern die Leistung von PHP-Anwendungen

In der Welt der Software spielt die Leistung eine Schlüsselrolle. Sie beeinflusst die Benutzererfahrung, die Kundenzufriedenheit und sogar die Gesamtrentabilität des Unternehmens. Dies gilt insbesondere für PHP-Anwendungen, die für ihre Einfachheit und Flexibilität bekannt sind, aber bei nicht optimaler Nutzung mit Leistungsproblemen konfrontiert sein können.

In diesem Artikel tauchen wir in die Welt der PHP-Anwendungsoptimierung ein. Wir werden untersuchen, wie Sie die Leistung Ihres Codes verbessern können, ohne seine Lesbarkeit oder Funktionalität zu beeinträchtigen. Wir werden verschiedene Strategien und Ansätze diskutieren, angefangen von elementaren Best Practices wie Outsourcing von Berechnungen und Caching bis hin zu komplexeren Themen wie Mikrooptimierung und Sequenzoptimierung.

Bevor wir jedoch beginnen, sei daran erinnert, dass Optimierung nicht immer ein Allheilmittel ist. Es ist wichtig, realistisch zu bleiben und zu verstehen, dass nicht alle Leistungsverbesserungen einen spürbaren Einfluss auf die Gesamtleistung Ihrer Anwendung haben. Daher ist es am besten, sich auf die Codebereiche zu konzentrieren, die tatsächlich spürbare Ergebnisse bringen können.

Wenn man an zuverlässige Software denkt, ist das erste, was einem in den Sinn kommt, die Verwendung von effizientem Code. Mit Hilfe von Optimierungen erstellen wir Algorithmen, die die gleiche Aufgabe mit wesentlich geringerem Aufwand erledigen. Dieser Gedanke ist in gewisser Weise die Grundlage für qualitativ hochwertige Programmierung.

Wir können uns den Alltag ohne Software nicht mehr vorstellen und wir wollen das auch nicht ändern. Wir sollten jedoch versuchen, so wenig Ressourcen wie möglich zu verbrauchen, um das gleiche Ergebnis zu erzielen.

Algorithmen sind die Grundlage für die grundlegende Ausbildung von Softwareentwicklern. Aber Hand aufs Herz - wie oft schreiben Sie selbst eine Sortierfunktion? Eben, fast nie. Viel häufiger verlassen wir uns auf bestehende Funktionen, die uns PHP bietet. Das ist sinnvoll, denn diese Funktionen sind geprüft und ausgereift, und wir werden in der Regel keine schnelleren Funktionen schreiben können.

Aber auch wenn wir solche spezialisierten Algorithmen normalerweise nicht selbst schreiben, gibt es immer noch eine Menge anderer Fehler, die manchmal die Leistung erheblich beeinträchtigen können. Besonders wenn es sich um riesige Schleifen handelt, können sich selbst kleine Leistungsverluste stark auf die Systemleistung auswirken.

Leider stellt sich immer wieder heraus, dass selbst elementare Best Practices nicht angewendet werden:

  • Outsourcing von Berechnungen: Gibt es Operationen, die nicht bei jedem Schleifendurchlauf ausgeführt werden müssen? Wenn ja, führen Sie die Operation einmal aus und speichern Sie das Ergebnis in einer Variablen.
  • Caching: Es muss nicht immer Redis sein, selbst einfaches In-Memory-Caching mit Hilfe eines Arrays kann sehr nützlich sein, um wiederholte Datenbankabfragen usw. zu vermeiden.
  • Sinnvolle Mikrooptimierung: Leistungsverbesserungen wie die Verwendung von einfachen Anführungszeichen anstelle von doppelten haben nur einen geringen Einfluss auf die Leistung, können aber in leistungskritischen Codeabschnitten einen kleinen Unterschied machen. Einen guten Überblick finden Sie beispielsweise in The PHP Benchmark.
  • Sequenzoptimierung: Selbst kleine Änderungen im Programmablauf können die Arbeitsgeschwindigkeit erhöhen, z. B. durch die Anordnung von Bedingungen in if-Abfragen in Abhängigkeit von der Ausführungsgeschwindigkeit. Bedingungen, die wenig Rechenzeit benötigen, sollten zuerst geprüft werden.

Bei aller Liebe zur Optimierung: Bleiben Sie realistisch! Wenn Sie mehrere Stunden damit verbringen, ein Skript minimal zu verbessern, das selten ausgeführt wird, wird das Unternehmen damit kaum genug Geld verdienen.

In Anlehnung an das bekannte Zitat von Donald Knuth "Vorzeitige Optimierung ist die Wurzel allen Übels" ist es besser, sich zuerst um die Codebereiche zu kümmern, die einen spürbaren Einfluss auf die Leistung haben.

Datenbanken

Relationale Datenbanken sind ebenfalls eine häufige Ursache für Leistungsprobleme in Anwendungen. Dies kann mit den folgenden Grundregeln behoben werden:

  • Tabellen optimieren: Insbesondere Indizes werden oft vergessen, da ihr Fehlen erst bei großen Datenmengen, wie sie in großen Systemen vorkommen, durch Leistungseinbußen bemerkt wird.
  • Datensparen: Fordern Sie keine unnötigen Daten an, d.h. vermeiden Sie SELECT *, wenn nur wenige Spalten benötigt werden, und achten Sie auf sinnvolle LIMIT-Anweisungen.
  • Alternativen verwenden: Relationale Datenbanken sind nicht die beste Wahl für die Speicherung großer Mengen unstrukturierter Daten (z. B. Protokolle), da der Schreibzugriff teuer ist und vermieden werden sollte. Verwenden Sie stattdessen NoSQL-Alternativen, die diese Aufgabe viel besser bewältigen.

Dank objektorientierter Programmierung (OOP), Datenbankabstraktionen (DBAL) und objektrelationaler Abbildung (ORM) sind viele Abfragen tief unter den Codeebenen verborgen, die mehrere Meter dick sind, und schwer zu verstehen. Dies gilt insbesondere für Abfragen, die mit Tools wie Eloquent oder Doctrine erstellt wurden. Hier ist große Vorsicht geboten, da sonst die gleichen Abfragen mehrmals pro Thread ausgeführt werden.

Daher sollten Sie es sich zur Gewohnheit machen, alle Datenbankabfragen auf dem lokalen Entwicklungssystem zu protokollieren und diese Protokolle regelmäßig zu überprüfen. Tools wie die PHP Debug Bar lassen sich einfach in bestehende Anwendungen integrieren und zeigen, wie viele Abfragen pro Seitenaufruf gesendet werden oder wie schnell sie ausgeführt werden. Zur Identifizierung besonders langsamer Abfragen kann auch ein Protokollierungstool verwendet werden. Sehen Sie sich den EXPLAIN-Operator an, um fehlende Indizes zu finden.

Hier spielt natürlich die Bequemlichkeit eine große Rolle. Es ist bequem, mit einem einzigen Aufruf ein Array oder eine Sammlung von fertig initialisierten Objekten zu erhalten. Schließlich verwenden wir OOP und alle anderen Methoden sind das Steinzeitalter, oder? Aber die Leute übersehen, dass selbst die beste ORM Overhead hat und Konstruktorbibliotheken manchmal leistungsschwache Abfragen ausgeben.

Es ist sehr schlecht, wenn das Abfrageergebnis viele Datensätze enthält, da dann für jede Zeile ein separates Objekt instanziiert wird. Wenn wir Tausende von Objekten durchlaufen müssen, sollten wir uns nicht wundern, dass die Ausführung der Abfrage länger dauert.

Bei großen Datenmengen ist die direkte Verwendung von SQL-Abfragen, z. B. über PDO, oft die rationellere Wahl. Es ist nichts Falsches daran, gut durchdachte SQL-Abfragen und einfache Arrays zu verwenden, wenn wir dies sorgfältig kapseln, z. B. in einem Repository. Von der Leistungssteigerung profitieren nicht nur das System, sondern auch die Benutzer.

Engpasserkennung

Optimierung macht nur dann Sinn, wenn wir uns nicht einfach in der Unendlichkeit verlieren, sondern Messungen durchführen und die Ergebnisse vergleichen. Dazu sind nicht einmal spezielle Werkzeuge erforderlich. Wenn wir zum Beispiel herausfinden wollen, was besser ist - str_contains oder preg_match - um eine Substring zu finden, dann kann man das schnell selbst herausfinden:

<?php

declare(strict_types=1);

namespace App\Console\Commands;

use Illuminate\Console\Command;

final class CompareStringsCommand extends Command
{
    protected $signature = 'compare:strings';

    protected $description = 'Comparing performance of different commands';

    public function handle(): int
    {
        $start = microtime(true);
        for ($i = 0; $i <= 200000; $i++) {
            str_contains('Is there are any word Sergey here?', 'Sergey');
        }

        $end = microtime(true);
        $this->info( "result of str_contains(): ". sprintf('%.4f', $end - $start). "\n");

        $start = microtime(true);
        for ($i = 0; $i <= 200000; $i++) {
            preg_match("/php/i", 'Is there are any word Sergey here?');
        }

        $end = microtime(true);
        $this->info( "result of preg_match(): ". sprintf('%.4f', $end - $start). "\n");

        return 1;
    }
}

Der oben genannte Befehl in Laravel wird über php artisan compare:strings ausgeführt und gibt uns folgendes Ergebnis:

result of str_contains(): 0.0042

result of preg_match(): 0.0089

Absolute Zahlen sind hier nicht wichtig, aber im Verhältnis wird deutlich, dass str_contains in diesem speziellen Fall deutlich schneller ist. Das ist nicht überraschend, da preg_match für die Lösung dieser einfachen Aufgabe zu komplex ist. Es zeigt aber auch, dass die Wahl der Mittel gut überlegt sein sollte. Ein einzelner Aufruf mag keine Rolle spielen, aber in großen Mengen schon.

Übrigens, wie die Abbildung zum Post zeigt, liefert uns der Leistungsvergleich mit dem Tool PHPBench anschaulichere und genauere Kennzahlen. Die Testklasse hat eine sehr einfache Struktur.

<?php

declare(strict_types=1);

namespace Tests\Unit;

use PhpBench\Attributes as Bench;

class StringContainsBench
{
    #[Bench\Revs(2000)]
    #[Bench\Iterations(5)]
    public function benchStrContains(): void
    {
        str_contains('Is there Sergey string here?', 'Sergey');
    }

    #[Bench\Revs(2000)]
    #[Bench\Iterations(5)]
    public function benchPregMatch(): void
    {
        preg_match("/Sergey/i", 'Is there Sergey string here?');
    }
}

Die Attribute #[Bench\Revs] und #[Bench\Iterations] legen fest, wie oft Messungen wiederholt werden sollen, um Störungen bei der Bewertung zu vermeiden. Weitere Informationen finden Sie in der Projektdokumentation. Die Leistungsmessung so kleiner Codeabschnitte ist nur dann sinnvoll, wenn wir ungefähr wissen, welche Abschnitte problematisch sein könnten. Zur Suche danach sind Debugger und Profiler nützliche Werkzeuge.

Der wohl bekannteste Debugger in der PHP-Welt ist Xdebug. Mit seiner Hilfe können wir ein Programm zeilenweise ausführen. Eigentlich wird das Tool zur Fehlersuche verwendet, aber bei der schrittweisen Ausführung unseres Codes, dem Step-by-Step-Debugging, erhalten wir bereits einen ersten Eindruck davon, welche Codeabschnitte besonders langsam laufen.

Eine noch genauere Analyse kann mit einem Profiler durchgeführt werden. Er erfasst jeden Funktionsaufruf während der Programmausführung mit Angabe seiner Dauer und kann so ein detailliertes Bild der internen Abläufe erstellen. Wird eine langsame Funktion vielleicht besonders oft aufgerufen, oder gibt es irgendwo eine Schleife, an die man gar nicht gedacht hat?

Übrigens wird Xdebug zusammen mit einem Profiler geliefert. Wenn dieser aktiviert ist, wird für jeden Lauf eine Datei im Cachegrind-Format erstellt. Zum Auslesen wird jedoch ein anderes Tool benötigt. Wenn Ihre IDE dieses Format nicht unterstützt, gibt es Tools wie KCacheGrind oder QCacheGrind.

Unter Volllast

Im lokalen Entwicklungsmodus werden zum Testen meist nur kleine Datensätze verwendet. So bleiben viele Engpässe unentdeckt, bis sie von Kunden entdeckt werden, die beispielsweise nicht nur ein paar Dutzend, sondern Hunderttausende von CSV-Datenzeilen importieren möchten. Kommerzielle Tools wie NewRelic, Blackfire oder Tideways messen die Leistung komplexer Systeme und gehen bis ins kleinste Detail. Sie können wertvolle Hinweise darauf geben, wo genau Probleme auftreten.

Diese Tools sind jedoch nicht billig, weshalb sie oft vernachlässigt werden. Wenn Kunden mit echten Leistungsproblemen konfrontiert werden, werden sie hektisch gekauft. Sie wären jedoch viel nützlicher, wenn sie von Anfang an in das System integriert wären.

Dicke Bibliotheken

Pakete sollten mit Vorsicht ausgewählt werden, da sie in der Regel unter "Feature Creep" leiden: Nachdem sie im Laufe der Jahre unzählige Features aufgenommen haben, sind sie oft mit Funktionen überladen - und werden dementsprechend zu schwer.

Ein Beispiel ist das weit verbreitete Nesbot/Carbon. Der Name selbst ist natürlich thematisch sehr passend, aber er zeigt auch anschaulich, wie man es besser nicht macht. Carbon beansprucht, eine einfache API für native Datumsfunktionen in PHP bereitzustellen. Vielleicht ist das auch Geschmackssache. Wir wollen nicht über Geschmack streiten, aber wir sprechen über Leistung - und hier schneidet Carbon deutlich schlechter ab, wie der folgende Code zeigt, wiederum mit Hilfe von PHPBench.

<?php

declare(strict_types=1);

namespace Tests\Unit;

use Illuminate\Support\Carbon;
use PhpBench\Attributes as Bench;

class CarbonDateBench
{
    #[Bench\Revs(2000)]
    #[Bench\Iterations(5)]
    public function benchStrtotime(): void
    {
        strtotime('-30 days');
    }

    #[Bench\Revs(2000)]
    #[Bench\Iterations(5)]
    public function benchCarbon(): void
    {
        Carbon::now()->subDays(30)->getTimestamp();
    }

    #[Bench\Revs(1000)]
    #[Bench\Iterations(5)]
    public function benchDateTime(): void
    {
        (new \DateTime('-30 days'))->getTimestamp();
    }
}

Hier verwenden wir drei verschiedene Methoden, um den Zeitstempel für das Datum vor 30 Tagen zu ermitteln: die Funktion strtotime, das DateTime-Objekt (beides PHP-nativ) und Carbon. Wie aus dem Ergebnis hervorgeht, benötigt Carbon für die Lösung derselben Aufgabe in 8 Mikrosekunden 16 Mal mehr Ausführungszeit als die beiden alternativen Varianten mit je 0,5 Mikrosekunden.

root@sergey-HP-ZHAN-99:/var/www/html/laravel# vendor/bin/phpbench run /var/www/html/laravel/tests/Unit/CarbonDateBench.php --report=aggregate
PHPBench (1.2.14) running benchmarks... #standwithukraine
with configuration file: /var/www/html/laravel/phpbench.json
with PHP version 8.1.2-1ubuntu2.13, xdebug ❌, opcache ❌

\Tests\Unit\CarbonDateBench
 
    benchStrtotime..........................I4 - Mo3.240μs (±1.28%)
    benchCarbon.............................I4 - Mo11.327μs (±1.32%)
    benchDateTime...........................I4 - Mo4.297μs (±2.04%)

Subjects: 3, Assertions: 0, Failures: 0, Errors: 0
+-----------------+----------------+-----+------+-----+----------+----------+--------+
| benchmark       | subject        | set | revs | its | mem_peak | mode     | rstdev |
+-----------------+----------------+-----+------+-----+----------+----------+--------+
| CarbonDateBench | benchStrtotime |     | 2000 | 5   | 4.948mb  | 3.240μs  | ±1.28% |
| CarbonDateBench | benchCarbon    |     | 2000 | 5   | 5.409mb  | 11.327μs | ±1.32% |
| CarbonDateBench | benchDateTime  |     | 1000 | 5   | 4.948mb  | 4.297μs  | ±2.04% |
+-----------------+----------------+-----+------+-----+----------+----------+--------+

Auch hier gilt: Einzelne Aufrufe verursachen keine Probleme, aber bei Massenausführung macht es definitiv einen Unterschied. Und warum sollte man überhaupt eine deutlich langsamere Funktion verwenden, wenn die Alternativen nicht schlechter sind?

Aber es kommt noch schlimmer. Carbon belegt fast 5 MB Speicherplatz. Das liegt vor allem an den 180 Sprachdateien im Verzeichnis Lang, die zusammen 3,5 MB belegen. In der Regel werden in einer Anwendung nicht alle Sprachen der Welt unterstützt, so dass für unser Projekt im günstigsten Fall nur 500 Kilobyte an Sprachdateien benötigt werden. Die restlichen drei Megabyte an ungenutzten Sprachdateien sind trotzdem in jeder Installation enthalten.

Drei Megabyte scheinen nicht viel zu sein. Wenn man jedoch bedenkt, dass derzeit täglich etwa 200.000 Carbon-Programme installiert werden, verursacht dies mehr als 200 Terabyte unnötigen Netzwerkverkehr pro Jahr. Daher ist es in diesem Fall sinnvoll, die Sprachdateien auszulagern, so dass sie nur bei Bedarf geladen werden. Aus ähnlichen Gründen wurde übrigens das bekannte Paket fzaninotto/Faker erstellt, das vom ursprünglichen Entwickler nicht weiterentwickelt wurde.

Es kann immer Situationen geben, in denen die Verwendung von Carbon zur Lösung einer bestimmten Aufgabe erforderlich ist. Wenn möglich, sollte man jedoch versuchen, darauf zu verzichten. Zum Beispiel sind die nativen Datumsfunktionen in PHP oft völlig ausreichend und viel schneller, aber vielleicht nicht so komfortabel in der Anwendung.

Gibt es vielleicht eine kompaktere Alternative zum Paket? Zum Beispiel Guzzle - auch ein sehr häufig verwendetes Paket. Im Vergleich dazu hat Nyholm/PSR7 zum Beispiel viel weniger Code und bietet eine höhere Arbeitsgeschwindigkeit bei einem ähnlichen Funktionsumfang.

Was ist also als Erstes zu tun?

In diesem Artikel lassen sich unmöglich alle Möglichkeiten zur Leistungssteigerung von Anwendungen aufzählen, aber ich möchte Ihnen dennoch einige Denkanstöße geben:

Sparsame APIs: Senden Sie nur die absolut notwendigen Daten als Antwort. GraphQL-APIs haben hier aufgrund ihres Prinzips natürlich einen Vorteil, aber Daten können auch in RESTful-APIs gespart werden, indem zusätzliche Endpunkte für spezielle Anwendungsfälle bereitgestellt werden, in denen mehr Daten benötigt werden. Es ist auch möglich, den Clients die Möglichkeit zu geben, selbst zu entscheiden, welche Daten in die Antwort aufgenommen werden sollen, indem spezielle Anfrageparameter verwendet werden. Beispielsweise würde ein Aufruf von GET /user?fields=id,last_name,first_name den User-Endpunkt anweisen, nur die Felder id, last_name und first_name bereitzustellen. Serverseitig muss dies natürlich implementiert werden.

Komprimierung: Gzip-Komprimierung für Webseiten oder APIs spart Bandbreite und erhöht somit die Leistung. Klingt banal, wird aber oft vergessen.

Prozess-Build: Vergessen Sie nicht, Caching für CI/CD-Pipelines zu verwenden, insbesondere für Composer. Das spart viel wertvolle Rechenzeit.

Docker: Container schleppen oft unnötige Daten mit sich herum. Container auf Basis des sparsamen Alpine Linux sind um ein Vielfaches kleiner und werden daher schneller gebaut.

Composer: Entfernen unnötiger Composer-Pakete, die sonst bei jedem Build oder Composer-Update immer wieder neu installiert werden. Dabei können Tools wie composer-unused helfen.

Warum das Ganze?

Leider wird das Thema Leistung oft als zweitrangig behandelt. Dennoch sollte der Einfluss der Leistung auf Kosten, Kundenzufriedenheit und Geschäftsgewinn nicht unterschätzt werden. Eine frühzeitige Leistungsüberwachung kann die schlimmsten Folgen verhindern. Sobald der Code jedoch in der Produktion ist, ist Refactoring keine einfache Sache mehr. Am schlimmsten ist es wahrscheinlich, dem Produktmanager zu erklären, warum ein weiterer Sprint erforderlich ist, um eine bereits fertige, getestete und genehmigte Funktionalität unter Last nutzbar zu machen.

Im Laufe meiner Karriere bin ich auf unzählige Beispiele gestoßen, bei denen selbst kleine Verbesserungen schnell zu einer mehr als zehnfachen Beschleunigung des Codes führten.

Besonders in der Cloud können diese Leistungsprobleme auch kostspielig werden. Bei Verwendung von Auto-Scaling, d. h. der automatischen Anpassung der bereitgestellten Ressourcen an die aktuelle Last, werden diese Engpässe schnell durch Erhöhung der Rechenleistung kompensiert und bleiben zunächst unbemerkt, zumindest solange, bis sich jemand die Berechnungen genauer ansieht. Somit kann eine gute Leistung von Software auch finanziell von Vorteil sein.

Fazit

Leistungsoptimierung ist ein komplexer Prozess, der ein tiefes Verständnis sowohl der spezifischen Anwendung als auch der allgemeinen Prinzipien der Programmierung und des Systembetriebs erfordert. Die Ergebnisse dieses Prozesses können jedoch sehr beeindruckend sein. Geringerer Ressourcenverbrauch, schnelle Aufgabenerledigung, verbesserte Benutzererfahrung - all dies liegt im Bereich derer, die bereit sind, Zeit und Mühe in die Optimierung ihres Codes zu investieren.