Leitfaden für ein sicheres und schnelles WordPress-Setup

Keywords: #einstellungen #setup #wordpress

Das Thema Geschwindigkeit liegt bei uns allen ganz weit oben auf der Agenda. Oft allerdings nur passiv, wenn man ungeduldig auf der Tastatur trommelt und auf das Laden einer Internetseite wartet. Aktiv fehlt oft der richtige Impuls. Was kann man noch tun, außer die Bilder kleinzuhalten, um WordPress zu beschleunigen?

Hier möchte ich dir ein paar einfache Tricks vorstellen, um die Geschwindigkeit deiner WordPress-Seite zu optimieren. Und da neben der Geschwindigkeit die Sicherheit ein weiteres beliebtes Thema ist, werde ich auch dazu ein paar kleine Kniffe mit großer Wirkung zeigen. Doch zunächst ein paar…

Grundsätze

Es gibt ein paar Prämissen, die du beim Thema Geschwindigkeit und Sicherheit berücksichtigen solltest:

  • Backups - Nuff said. Du wirst dein System niemals 100% absichern können. Umso wichtiger sind Backup.
  • Plugin-Sparksamkeit - jedes zusätzliche Plugin ist nicht nur ein potentielles Sicherheitsrisiko, sondern lässt deine Seite auch unnötig anwachsen. Versuche so viele Funktionen wie möglich selber umzusetzen. Ein Child-Theme erlaubt dir in der functions.php nicht nur Anpassungen des Layouts sondern auch des Funktionsumfanges vorzunehmen.
  • Updates - Halte WordPress, Plugins und Themes immer auf dem neuesten Stand. Verzichte möglichst auf Plugins und Themes, die lange nicht aktualisiert wurden.
  • Page Builder - ich bin wahrlich kein Fan von Page-Buildern, auch wenn es da draußen relativ performante Vertreter dieser Art gibt. Allerdings wohnt jedem Page-Builder in der Regel ein Problem inne: Sie kommen mit einer Menge von Funktionen, die du oft nicht benötigst, die aber trotzdem Ressourcen verbrauchen.

Nun, da das geklärt ist und ich meinen Unmut über Page-Builder mal wieder unterbringen konnte: Was kannst du aktiv tun, um die Geschwindigkeit und Sicherheit zu optimieren? Im Folgenden werden wir Änderungen an drei Dateien vornehmen:

  • functions.php (deines Child-Themes)
  • .htaccess-Datei
  • wp-config.php

Du findest alle drei Dateien auf github.com.

Warnung

Da es sich mitunter um sehr tiefe Eingriffe in die WordPress-Mechanik handelt, folgender wichtiger Hinweis:

Nehme die hier beschriebenen Änderungen niemals an einer Live-Installation vor; teste sie in einer geschützten Umgebung und übertrage sie dann sorgfältig und ggf. nacheinander in das Live-System!

Lege außerdem immer ein Backup von den Dateien an, die du im Laufe dieses Artikels ändern wirst.

Diese Anleitung ist ein Leitfaden, den du nicht ungelesen übernehmen solltest. Passe die Änderungen an die Anforderungen deines Projektes an. Klar soweit? Dann los:

Nutze keine Standard-Einstellungen

Eigentlich gehört diese Weisheit zu den Prämissen, ich will sie hier trotzem etwas ausführlicher erklären. Dieser Punkt gehört für mich zum Konzept “Secruity through obscurity”. Viele vermeintliche Angriffe auf deine Seite sind nur das Grundrauschen: Automatisierte Scripte, die zahlreiche Webseiten nach bekannten Sicherheitslücken abklopfen. Lass uns das “passive Angriffe” nennen. Du kannst das geduldig über dich ergehen lassen oder schon jetzt dafür sorgen, dass deine Seite gar nicht erst im Rampenlicht steht. Wenn bei den passiven Angriffen keine Lücken erkannt werden, zieht der Bot weiter und widmet sich schwächeren Seiten. Das Credo lautet “Kosten-Nutzen-Analyse”. Warum mit einem unbekannten Ziel beschäftigen, wenn da draußen genug unsichere WordPress-Installationen darauf warten, gehackt zu werden? Also:

  • Nutze als Benutzername für den Administrator-Account nicht den Standardwert “admin”. Erzeuge einen neuen Admin-Nutzer (z.B. MyLittlePony) mit allen Admin-Rechten und entferne den originalen Admin-Nutzer
  • Nutze ein “kryptisches” Tabellen-Prefix (z.B. x02349d_), um das Auffinden der WordPress-Tabellen in der Datenbank zu erschweren. Bei Angriffen über SQL-Injection wird oft vom Standard-Prefix (wp_) ausgegangen.
  • Ach ja, und nein: Verschiebe nicht die wp-admin-URL - das erzeugt mehr Unruhe im System, als dass es nutzt. Hier reicht der zusätzliche Schutz mit HTTP Basic Auth.
  • Setze korrekte Dateirechte ein, um zu vermeiden, dass jemand deine PHP-Dateien ändern und Schadcode einfügen kann (das ist ein etwas größeres Thema auf Serverseite, was hier ganz gut beschrieben ist).
  • Kein Standard-Passwort. Ok. Muss ich das wirklich erwähnen? ;)

Emoticons und Emojis

Emojis sind zwar unstrittig ein fester Bestandteil der modernen Kommunikation, was nicht heißt, dass man sie überall nutzen muss. In WordPress gehören sie leider zum Standard, was die Ladezeit verringert. Um sich der kleinen Kecker zu entledigen, ist eine ganze Menge PHP-Code (lose basierend auf diesem Beitrag):

function disable_emojis() {
    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
    remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
    remove_action( 'wp_print_styles', 'print_emoji_styles' );
    remove_action( 'admin_print_styles', 'print_emoji_styles' );
    remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
    remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
    remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
    add_filter( 'tiny_mce_plugins', 'disable_emojis_tinymce' );
    add_filter( 'wp_resource_hints', 'disable_emojis_remove_dns_prefetch', 10, 2 );
}

function disable_emojis_tinymce( $plugins ) {

    if ( is_array( $plugins ) ) {
        
        return array_diff( $plugins, array( 'wpemoji' ) );

    } else {
        
        return array();

    }
}
 
function disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
    if ( 'dns-prefetch' == $relation_type ) {
    
        /** This filter is documented in wp-includes/formatting.php */
        $emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/2/svg/' );

        $urls = array_diff( $urls, array( $emoji_svg_url ) );
    }

    return $urls;
}    

function disable_emojicons_tinymce( $plugins ) {

    if ( is_array( $plugins ) ) {
    
        return array_diff( $plugins, array( 'wpemoji' ) );

    } else {

      return array();

    }
}

function disable_wp_emojicons() {

    remove_action( 'admin_print_styles', 'print_emoji_styles' );
    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
    remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
    remove_action( 'wp_print_styles', 'print_emoji_styles' );
    remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
    remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
    remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );

Hier passiert eine ganze Menge, weil die Emojis relativ breit implementiert wurden. Die einfachste Erklärung ist: Wir entfernen alle möglichen Verweise auf Emojis. Du musst die Trigger allerdings noch aktivieren indem du sie folgendermaßen aufrufst:

add_filter( 'tiny_mce_plugins', 'disable_emojicons_tinymce');}
add_action( 'init', 'disable_emojis' );
add_filter( 'emoji_svg_url', '__return_false' );
add_action( 'init', 'disable_wp_emojicons' );

Gravatar deaktivieren

Du kannnst die Nutzung der Gravatare auch in den Einstellungen deaktivieren. Ich bevorzuge aber die Variante in der functions.php, da man sich so ein kleines Boilerplate erstellen kann. So deaktivierst du die Gravatar-Funktion:

add_filter( 'option_show_avatars', '__return_false' );

Wie du siehst, spart uns das mindestens zwei Anfragen und ein paar Millisekunden ein:

GZIP und Deflate

Sehr naheliegend ist natürlich die Komprimierung von Dateien. Du nimmst diese Änderung in der .htaccess-Datei vor:

<IfModule mod_deflate.c>
    <FilesMatch ".*\.(html|php|css|js|xml)$">
        SetOutputFilter DEFLATE
    </FilesMatch>
    AddOutputFilterByType DEFLATE application/rss+xml
    AddOutputFilterByType DEFLATE application/xml application/xhtml+xml
    AddOutputFilterByType DEFLATE application/javascript application/x-javascript
</IfModule>

Achtung: Du solltest deine Seite genau beobachten. Die Aktivierung von GZIP führt bei sehr kleinen Dateien nicht zu einem Größenvorteil, kann sich aber negativ auf die Antwortzeit des Servers auswirken, da die kleinen Dateien ja trotzdem einmal “angefasst” werden.

Cron-Job deaktivieren

Bei jedem Aufruf der Seite wird ein internes Cron-Job-Script ausgeführt. Das ist vor allem für das Suchen nach Updates wichtig. Wenn dein Hoster Cron-Jobs anbietet, ist das natürlich unnötig und verlangsamt jeden Aufruf deiner Seite. Du kannst die Cron-Job-Aufrufe in der wp-config.php folgendermaßen deaktivieren:

define('DISABLE_WP_CRON', true);

Danach musst diese Anfrage aber trotzdem irgendwie ausführen. Das machst du im Backend deines Hosters, bei all-inkl z.B. im KAS unter Tools:

Als Intervall genügt hier eigentlich stündlich.

In der Regel müssen JavaScript-Dateien nicht sofort zur Verfügung stehen, da sie z.B: Benutzerinteraktion ermöglichen bzw. darauf reagieren. Trotzdem können Sie den Aufbau der Seite verzögern, wenn sie ganz am Anfang oder in der Mitte eingebunden werden.

Aus dem Grund sollte man die Scripte ganz am Ende einer Seite auflisten, die nicht für den Seitenaufbau direkt benötigt werden. Die grobe Methode funktioniert folgendermaßen:

add_action('after_setup_theme', 'footer_enqueue_scripts');
function footer_enqueue_scripts() {
    remove_action('wp_head', 'wp_print_scripts');
    remove_action('wp_head', 'wp_print_head_scripts', 9);
    remove_action('wp_head', 'wp_enqueue_scripts', 1);
    add_action('wp_footer', 'wp_print_scripts', 5);
    add_action('wp_footer', 'wp_print_head_scripts', 5);
    add_action('wp_footer', 'wp_enqueue_scripts', 5);
}

Nachdem du diese Zeilen übernommen hast, solltest du deine Seite einmal z.B. im Inkognito-Modus, ohne Cache, neu laden. Flackert die Seite in der ersten Millisekunden auffällig, also wird für einen kurzen Augenblick der fast roh anmutende Inhalt der Seite und dann das Layout angezeigt, kommentiere in der Callback-Funktion die beiden Zeilen aus, die wp_enqueue_scripts in den Footer verlagern.

JavaScript Ausführung verzögern

Die etwas elegantere Methoden nennen sich “defer” und “async”. Mit defer teilst du dem Browser mit, dass die JavaScript-Datien im Hintergrund geladen und erst ausgeführt werden, wenn die eigentlichen Inhalte fertig sind.

add_filter( 'script_loader_tag', 'defer_parsing_of_js', 10 );
function defer_parsing_of_js( $url ) {
    
    if ( is_user_logged_in() ) return $url; //don't break WP Admin
    
    if ( strpos( $url, '.js' ) === FALSE) return $url; // only process JavaScript files
    
    if ( strpos( $url, 'jquery.js' ) ) return $url;  // skip JQuery
    
    return str_replace( ' src', ' defer src', $url );

}

Der defer-Flag macht nur bei Scripten Sinn, die mit src eingebunden werden. Inline-Scripte werden immer sofort gelesen und geparsed. Daneben gibt es noch async. Mit async wird die Datei ebenfalls parallel geladen und sofort ausgeführt, wenn die Datei vollständig ist. Das macht vor allem bei Tracking-Scripten Sinn, die keinen Bezug zum inhaltlichen Aufbau haben.

Versionsinfos entfernen

Style- und Script-Dateien werden oft mit einem Anhang übermittelt, z.B. style.css?ver=123. Das macht Sinn um Änderungen nachzuverfolgen, hat aber auch einen entscheidenen Nachteil: Damit werden Cache-Mechanismen umgangen, weil der Cache denkt, es handelt sich um eine dynamische Ressource. Aus dem Grund solltest du die Versions-Infos komplett deaktivieren. Achte beim Arbeiten an der Webseite einfach daran, sämtliche Caches zu deaktivieren.

add_filter( 'script_loader_src', 'remove_version_parameter', 15, 1 );
add_filter( 'style_loader_src', 'remove_version_parameter', 15, 1 );
function remove_version_parameter($src){

    // Check if version parameter exist
    $parts = explode( '?ver', $src );
    
    // return without version parameter
    return $parts[0];
    
}

Google Fonts deaktivieren

Die Einbindung der Fonts über Google ist bequem, birgt neben dem Performance-Nachteil aber unter Umständen auch ein Datenschutz problem. (Grundsätzlich solltest du versuchen, alle Ressourcen von deinem Server zu laden und auch auf CDN zu verzichten - das bringt selten einen Vorteil.)

Leider ist das Entfernen nicht ohne weiteres möglich. Du musst erst herausfinden, unter welchem Handler das Theme die Google Fonts einbindet (Eine Liste bekannter Themes und der verwendeten Font-Handler findest du hier.) Dazu durchsuchst du die functions.php nach z.B. Google. Für das Theme Rowling sieht das dann folgendermaßen aus:

wp_register_style( 'rowling_google_fonts', '//fonts.googleapis.com/css?family=Lato:400,700,900,400italic,700italic|Merriweather:700,900,400italic' );

Der Handler lautet also “rowling_google_fonts”. Auf gehts:

add_action( 'wp_print_styles', 'dequeue_google_fonts_style' );

function dequeue_google_fonts_style() {

      wp_dequeue_style( 'rowling_style' );

}

Achte darauf, dass jetzt natürlich die Fallback-Schriftart aus dem CSS verwendet wird.

Die Beitrags-Historie verschlanken

Seit einiger Zeit gibt es in WordPress das ansich ganz nützliche Feature der Revisionen: Bei jeder Änderung und jeder Speicherung legt WordPress in der Datenbank eine Kopie der vorherigen Version des Beitrages an. Das ist praktisch. Aber auch grenzenlos.
Zunächst solltest du die Anzahl der Revisionen pro Beitrag eingrenzen. Dazu setzt du in der wp-config.php folgenden Parameter:

define('WP_POST_REVISIONS', 5);

Die Fünf beschreibt die Anzahl der erlaubten Revisionen. Du kannst die Zahl natürlich beliebig anpassen. Damit ist es aber noch nicht getan, du solltest die alten Revisionen natürlich noch aufräumen. Dazu kannst du dir das Plugin WP Sweep installieren. Du kannst nun ganz bequem alle Revisionen löschen:

Alle Revisionen mit WP Sweep löschen

oEmbeds deaktivieren

Hierbei handelt es sich um ein Feature, das WordPress seit Version 4.4 mitbringt, die sogenannten Embeds. Dir ist es vielleicht schon mal aufgefallen: Beim Einfügen von URL in den Beitrag, wird nicht die URL angezeigt, sondern eine Art Snippet mit zusätzlichne Informationen und sogar einer Vorschau. Das sieht schick aus, braucht aber vielleicht nicht jeder. Wenn du Embed also nicht benötigst, erspart dir das wieder ein paar Zeilen im HTML-Header und sogar eine zusätzliche JavaScript-Bibliothek:

Die Embed-Bibliothek von WordPress

Füge dazu folgendes in die functions.php ein (Quelle):

add_action( 'init', 'disable_embeds_code_init', 9999 );

function disable_embeds_code_init() {
    remove_action( 'rest_api_init', 'wp_oembed_register_route' );
    add_filter( 'embed_oembed_discover', '__return_false' );
    remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 );
    remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
    remove_action( 'wp_head', 'wp_oembed_add_host_js' );
    add_filter( 'tiny_mce_plugins', 'disable_embeds_tiny_mce_plugin' );
    add_filter( 'rewrite_rules_array', 'disable_embeds_rewrites' );
    remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 );
}

function disable_embeds_tiny_mce_plugin($plugins) {
    return array_diff($plugins, array('wpembed'));
}

function disable_embeds_rewrites($rules) {
    foreach($rules as $rule => $rewrite) {
        if(false !== strpos($rewrite, 'embed=true')) {
            unset($rules[$rule]);
        }
    }
    return $rules;
}

Den Header aufräumen

Kommen wir zu ein paar Maßnahmen, die das HTML ein wenig verschlanken, aber aus Performance-Sicht nur Peanuts sind: Wir räumen den Header-Bereich auf. Diese Maßnahmen sind nicht wirklich notwendig, außer du willst es wirklich auf die Spitze treiben.

Los geht es mit dem Manifest für den Windows Live Writer, einer App um Blog-Beiträge zu schreiben (was mit deaktivierter XML-RPC, siehe unten, sowieso nicht mehr möglich ist):

<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://example.com/wp-includes/wlwmanifest.xml">

Und so deaktivierst du den Spaß:

remove_action( 'wp_head', 'wlwmanifest_link');

Weiter geht es mit dem Verweis zum Shortlink des jeweiligen Beitrages. Shortlinks sind praktisch, um mit kurzem URL auf Inhalte deiner Webseite zu verweisen. Anstatt dem langen Permalink www.example.com/das-ist-mein-allererster-beitrag-hier-und-ich-liebe-es nutzt du z.B. einfach www.example.com/?p=1. Wenn du das nicht brauchst, entferne den Shortlink-Verweis aus dem HTML-Header und dem HTTP-Antwort-Header:

Der Shortlink-Verweis im HTTP-Header

remove_action( 'wp_head', 'wp_shortlink_wp_head');

add_filter('after_setup_theme', 'remove_shortlink_from_http_header');

function remove_shortlink_from_http_header() {

    remove_action( 'template_redirect', 'wp_shortlink_header', 11);

}

Im Header findest Verweise auf den vorherigen, den nächsten Beitrag oder die Startseite. Diese Funktionalität sollte theoretisch durch dein Template abgedeckt sein. Diese Verweise entfernst du folgendermaßen:

remove_action('wp_head', 'start_post_rel_link');
remove_action('wp_head', 'index_rel_link');
remove_action('wp_head', 'adjacent_posts_rel_link');

Zu guter Letzt die Versions-Information. Das Verbergen der WordPress-Version kann auch als Sicherheitsfeature verstanden werden (Security through obscurity, Sicherheit durch Unklarheit, siehe oben). Wenn der potentielle Angreifer nicht weiß, welche Version du verwendest, erschwerst du ihm zumindest das Identifizieren potentieller Sicherheitslücken. In der Realtität wird das nur die Bots aufhalten, die dein System automatisiert scannen. Bei einem aktiven Angriff ist das ziemlich sicher nutzlos. Nutze dazu folgende Zeile in der functions.php:

remove_action('wp_head', 'wp_generator');

Cache-Plugin

Wenn du immer noch nicht zufrieden bist, kannst du auch auf ein Cache-Plugin zurück greifen. Das sorgt dafür, dass die Seiten nicht bei jedem Aufruf komplett neu über PHP erstellt, sondern statische Inhalte ausgeliefert werden. Das bringt noch mal einen enormen Geschwindigkeitsgewinn.

Die XML-RPC-Schnittstelle deaktivieren

XML-RPC steht für Extensible Markup Language Remote Procedure Call. Klingt kompliziert, ist es auch. Dabei handelt es sich um eine Schnittstelle. mit der du, ganz einfach gesagt, WordPress steuern kannst, ohne auf das Backend zuzugreifen (mehr Hintergründe dazu hier). Wenn du diese Schnittstelle deaktivierst, verlierst du also Funktionalität, wie z.B. Pingbacks oder das Verwalten von WordPress mit einer externen App. Auch Jetpack greift auf die XML-RPC zu! Du gewinnst aber auch etwas an Sicherheit dazu. Entscheide selber.

In den aktuellen Apache-Versionen (ab 2.4) sperrst du den Zugriff auf xmlrpc.php in der .htaccess-Datei folgendermaßen:

<FilesMatch "(^\.|wp-config\.php|xmlrpc\.php|(?<!robots)\.txt|(liesmich|readme)\.*)">
   Require all denied
</FilesMatch>

Eigentlich sollte das schon genügen. Willst du auf Nummer sicher gehen, kannst du durchaus mehrt tun. Der weniger disruptive Weg wäre, alle Methoden zu deaktivieren, die eine Authentifizierung erfordern. Das funktioniert folgendermaßen (in der functions.php deines Child-Themes):

# XML RPC deaktivieren
add_filter( 'xmlrpc_enabled', '__return_false' );
# XML RPC Verweis aus Header entfernen
remove_action('wp_head', 'rsd_link');

Allerdings ist das ganze Sub-System weiterhin aktiv. Willst du das also komplett deaktivieren, füge der functions.php folgendes hinzu:

// disable xmlrpc
function remove_xmlrpc_methods( $methods ) {
  return array();
}
add_filter( 'xmlrpc_methods', 'remove_xmlrpc_methods' );

Aber wie gesagt, das sind zusätzliche Maßnahmen. In der Regel kannst du vielleicht schon gut schlafen, wenn der Webserver den Zugriff auf xmlrpc.php einfach nicht zulässt.

Die REST-API-Verweise entfernen

Seit Version 4.4 hat WordPress eine REST-API an Bord, die wie XML-RPC eine Schnittstelle bietete, um bestimmte Informationen über deine Seite automatisiert auszulesen. Obgleich das Sicherheitsrisiko der REST-API ungleich niedriger ist als bei XML-RPC, kannst du auch hier ein wenig aufräumen, indem du den Verweis auf die REST-API folgendermaßen aus den HTML- sowie HTTP-Header entfernst:

remove_action('wp_head', 'rest_output_link_wp_head', 10);
remove_action('template_redirect', 'rest_output_link_header', 11, 0);

Ganz deaktivieren solltest du die API allerdings nicht, da sie vor allem auch für die reibungslose Backend-Funktionalität wichtig ist. Was du aber machen kannst, ist die REST-API nur nach Anmeldung zulassen:

add_filter('rest_authentication_errors', 'rest_api_auth');

function rest_api_auth($result) {
    // bereits erfolgreich authentifiziert?
    if ( true === $result || is_wp_error( $result ) ) {
        return $result;
    }
 
    // noch nicht authentifiziert?
    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_not_logged_in',
            __( 'You are not currently logged in.' ),
            array( 'status' => 401 )
        );
    }
 
    // sonst zurück
    return $result;
}

Fazit

Das war es erstmal. Wenn du jetzt noch das Gefühl hast, dass dein WordPress zu langsam ist, wirf mal einen Blick in die Entwickler-Konsole und schau nach, welche Ressourcen lange dauern oder ob du Anfragen doppelt absetzt. Es gibt vor allem auf Server-Seite, also unterhalb von WordPress, noch eine Menge Möglichkeiten. Einige davon habe ich in der Artikel-Serie zum perfekten Web-Server-Setup aufgeführt. Außerdem gibt es hier eine wirklich brilliante Artikel-Serie zum Thema Sicherheit, die du dir unbedingt lesen solltest. Hier werden unzählige wichtige Hinweise gegeben,

Zum Abschluss noch mal der Hinweis: Es handelt sich hierbei um mitunter tiefe Eingriffe in die WordPress-Mechanik. Solltest du in der Zukunft Probleme mit deiner Seite haben, nimm diese Änderungen Schritt für Schritt zurück. Arbeite mit einem Staging-System und nutze Backups. Bei Probleme und Inkompatiblitäten freue ich mich über sachdienliche Hinweise, die ich in den Artikel einbauen kann!