Wordpress Speed Optimization Without Plugins

Optimize WordPress Site Speed Without Any Plugins

In today’s digital landscape, website speed has become more and more important. The average website loads on desktop in 10.3 seconds and loads in 27.3 seconds on mobile. To put it simply, the average website is slow. When you couple this with the fact that roughly one third of all websites are built with WordPress you can start to see how important optimization is. The faster your site loads the more visitors you will retain. Google states that conversion rates on average drop by 12% for every additional second your site takes to load.

In today’s post I am going to show you techniques for improving the speed of your WordPress site without the use of any plugins. There are many plugins out there that can help you do similar things however I believe that having full control of the process not only improves your understanding but ultimately leads to better results.

To start we should test the current speed of our website before implementing any speed optimization. We will be using two metrics for our testing. Google Page Insights and WebPage Test. I am using the elementor and elementor pro page builder with WordPress. I’ve added a template webpage that I will use to test the speed. My initial Google Page Insight score:

Mobile Score:

Page Insights Mobile Score Pre Optimization

Desktop Score:

Page Insights Desktop Score Pre Optimization

And our WebPageTest results before optimization:

Web Page Test Results Pre Optimization

Before we begin our optimizations, if you are running an Apache server you may have pagespeed installed on the server. Pagespeed is a set of tools published by Google that are meant to improve website speed. The pagespeed set of tools implements some of the techniques we cover in this article. I have noticed that some of the pagespeed features do not work with WordPress installations so we have chosen to disable the scripts and handle the optimizations on our own.

To disable pagespeed in Apache go to your pagespeed.conf file. Ours is located at /opt/bitnami/apache2/conf/pagespeed.conf. Find this line:

ModPagespeed on

And change to:

ModPagespeed off

Combining CSS and JS Files

The first thing we are going to do is add PHP code to our functions.php file for combining JS and CSS. This code will combine the majority of our JavaScript files into one larger file. It will also combine our CSS into one larger file. We will add the CSS to the head of our document and the JavaScript to the footer of the document. For the css we can use this code placed at the bottom of our functions.php file:

add_action( 'wp_enqueue_scripts', 'combine_all_styles', 9999 );
function combine_all_styles() {
  global $wp_styles;
  // Order handles by dependency
  $wp_styles -> all_deps($wp_styles -> queue);
  // New file location
  $combined_file_location = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'combined-styles.css';
  $combined_styles = '';
  // Loop through script handles and write to $combined_styles
  foreach( $wp_styles -> to_do as $handle) {
    // Remove query parameters from string
    $src = strtok( $wp_styles -> registered[ $handle ] -> src, '?' );
    if ( strpos ( $src, 'http' ) !== false ) {
      $site_url = site_url();
      if ( strpos ( $src, $site_url ) !== false )
        $file_path = str_replace ( $site_url, '', $src );
        $file_path = $src;
        $file_path = ltrim ($file_path, '/' );
    } else {
    $file_path = ltrim ( $src, '/' );
    // Check to see if the file exists then combine
    if  ( file_exists ( $file_path ) ) {
      // Check for wp_localize_script
      $localize = '';
      if ( @key_exists( 'data', $wp_styles -> registered[ $handle ] -> extra)) {
        $localize = $obj -> extra['data'] . ';';
      $combined_styles .=  $localize . file_get_contents ( $file_path ) . ';';
  // close foreach loop
  // write the combined script into current theme directory
  file_put_contents ( $combined_file_location , $combined_styles );
  // Load the URL of combined file
  wp_enqueue_style( 'combined-styles',  get_stylesheet_directory_uri() . '/combined-styles.css');
  // Deregister handles
  foreach( $wp_styles->to_do as $handle ) {

For the JavaScript portion you can add:

add_action( 'wp_enqueue_scripts', 'combine_all_scripts', 9999 );
function combine_all_scripts() {
  global $wp_scripts;
  $exceptions = array();
  // Order handles by dependency
  $wp_scripts -> all_deps($wp_scripts -> queue);
  // New file location eg: wp-content/themes/maintheme/js/combined-script.js
  $combined_file_location = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'combined-script.js';
  $combined_script = '';
  // Loop through script handles and write to $combined_script
  foreach( $wp_scripts -> to_do as $handle) {
    if (in_array($handle, $exceptions)) {
    // Remove query parameters from string
    $src = strtok( $wp_scripts -> registered[ $handle ] -> src, '?' );
    if ( strpos ( $src, 'http' ) !== false ) {
      $site_url = site_url();
      if ( strpos ( $src, $site_url ) !== false )
        $js_file_path = str_replace ( $site_url, '', $src );
        $js_file_path = $src;
        // Remove trailing forward slash from path
        $js_file_path = ltrim ($js_file_path, '/' );
    } else {
     $js_file_path = ltrim ( $src, '/' );
    // Check to see if the file exists then combine
    if  ( file_exists ( $js_file_path ) ) {
      // Check for wp_localize_script
      $localize = '';
      if ( @key_exists( 'data', $wp_scripts -> registered[ $handle ] -> extra)) {
      $localize = $obj -> extra['data'] . ';';
      $combined_script .=  $localize . file_get_contents ( $js_file_path ) . ';';
  // close foreach
  // write the combined script into current theme directory
  file_put_contents ( $combined_file_location , $combined_script );
  // Load the URL of combined file
  wp_enqueue_script( 'combined-script',  get_stylesheet_directory_uri() . '/combined-script.js', array(), false, true );
  // Deregister handles
  foreach( $wp_scripts->to_do as $handle ) {
    if (in_array($handle, $exceptions)) {

Adding our JavaScript to the footer instead of the head can improve our load time but also can create issues. Some JavaScript files still need to be added in the head for the page to load correctly. The code below will generate a new file called script-handles.txt in the root of your WordPress installation that will show us the handle names of each script loaded in the head.

add_action( 'wp_head', 'show_head_scripts', 9998 );
// Javascript handles before the header
function show_head_scripts(){
  global $wp_scripts;
  $scripts = fopen(ABSPATH . "/wp-content/themes/hello-elementor/script-handles.txt", 'wb');
  foreach( $wp_scripts -> done as $script) {
    fwrite($scripts, $script . PHP_EOL);

If your page is not loading correctly you need to determine which handles from ‘script-handles.txt’ need to be added to the $exceptions array in the Combine JS Function. Simply add the handle as a string to the array. For example JQuery needs to be loaded in the head for certain websites. We would add this to the array:

function combine_all_scripts() {
  global $wp_scripts;
  exceptions = array('jquery');

If you are stuck trying to add exceptions to the array and the page is still not loading correctly you can instead load the combined js script in the header by changing this code:

wp_enqueue_script( 'combined-script',  get_stylesheet_directory_uri() . '/combined-script.js', array(), false, true );

To this:

wp_enqueue_script( 'combined-script',  get_stylesheet_directory_uri() . '/combined-script.js');

If you refresh your page, inspect element and go to the Network tab of your browser you should see that the majority of your scripts and styles have been combined into one file. On our website we were originally loading 35 scripts and styles. This number was reduced to 11:

Network Tab with Combined JS and CSS

You can see the ‘combined-script.js’ and ‘combined-styles.css’ files being loaded. After adding this code our Google Page Insights score improved from 35 to 54 on mobile and and from 74 to 91 on desktop. Our WebPageTest load time changed from 12.2 seconds to 3.6 seconds.

GZip Compression

Our next step involves adding GZip Compression. This step will compress the files that are sent from the server to the client requesting them. The browser will decompress the files that are in GZip format meaning we are able to transfer less data from the server to the browser.

If you are using an Apache server you may already have compression enabled. You can locate the ‘apache2’ folder on your server and go to ‘conf’. In the ‘conf’ folder you may see a ‘deflate.conf’ file. This file would be included in your ‘httpd.conf’ file and would handle the compression for you. On my server the ‘apache2’ folder path is ‘/opt/bitnami/apache2’.

To implement this setting we will be editing the .htaccess file which is a configuration file for use on web servers running the Apache Web Server software. This file is located at the root of your WordPress installation. When you open the file you will see that some directives have already been added. These control the url structure of your website.

Htaccess Permissions

In the terminal at the bottom of the image you can see that I displayed the permissions of the file which are by default 644. We will need to change these permissions in order to save any changes.

First, let’s add those changes. Add the following code to your .htaccess file:

  # Compress HTML, CSS, JavaScript, Text, XML and fonts
  AddOutputFilterByType DEFLATE application/javascript
  AddOutputFilterByType DEFLATE application/rss+xml
  AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
  AddOutputFilterByType DEFLATE application/x-font
  AddOutputFilterByType DEFLATE application/x-font-opentype
  AddOutputFilterByType DEFLATE application/x-font-otf
  AddOutputFilterByType DEFLATE application/x-font-truetype
  AddOutputFilterByType DEFLATE application/x-font-ttf
  AddOutputFilterByType DEFLATE application/x-javascript
  AddOutputFilterByType DEFLATE application/xhtml+xml
  AddOutputFilterByType DEFLATE application/xml
  AddOutputFilterByType DEFLATE font/opentype
  AddOutputFilterByType DEFLATE font/otf
  AddOutputFilterByType DEFLATE font/ttf
  AddOutputFilterByType DEFLATE image/svg+xml
  AddOutputFilterByType DEFLATE image/x-icon
  AddOutputFilterByType DEFLATE text/css
  AddOutputFilterByType DEFLATE text/html
  AddOutputFilterByType DEFLATE text/javascript
  AddOutputFilterByType DEFLATE text/plain
  AddOutputFilterByType DEFLATE text/xml
  # Remove browser bugs (only needed for really old browsers)
  BrowserMatch ^Mozilla/4 gzip-only-text/html
  BrowserMatch ^Mozilla/4\.0[678] no-gzip
  BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
  Header append Vary User-Agent

Before you can save, change the permissions of the file with: ‘sudo chmod 777 .htaccess’ . After saving change the permissions back: ‘sudo chmod 644 .htaccess’.

Image Compression

Next we are going to optimize the size of our images to improve load time. Large image files can significantly slow down the speed of your website. Basically we will be downloading all of our image files to our computer and using an image compression tool to reduce their size.

Size After Image Compression

Let’s download our uploads folder onto our desktop so that we can compress our images. You can do this either via FTP or SSH. We use AWS Lightsail which has Bitnami installed on the instance. There is an extremely helpful remote ssh tool that we use to connect to our server. The extension is for VS Code but there may be something similar in other code editors. If you are interested in the tool check out this article: https://code.visualstudio.com/blogs/2019/07/25/remote-ssh

You may notice that in your image folders you have several different size versions for each image. These images are generated automatically by wordpress and your theme when you upload a new image. They can quickly make it hard to navigate your images. I generally disable this functionality. If you are interested in doing so you can check out Disable WordPress From Creating Different Image Sizes.

There are various tools you can download onto your desktop to handle image compression or you can use an online tool. For small batches I like to use an online tool: https://www.iloveimg.com/compress-image. There is a 15 image limit per compression. For large batches I use https://optimage.app/. It is convenient because you can drag your entire uploads folder into the load box and handle all of your images at once. The free version has a daily limit of 24 images. The paid version is not too expensive at $15 total.

Once you have completed compressing all of your images you can load the uploads folder to your server replacing the folder that already exists. After we compressed our images in our uploads folder and reuploaded it to the server our mobile score improved from 54 to 74 and our desktop score improved from 91 to 94.


When it comes to caching you have the option to cache files in the browser and also cache files on the server. Browser caching makes repeat visits to a website much faster as the files are already on the persons machine. Server caching improves first byte time which is the amount of time it takes your browser to start downloading content from the server.

Caching Diagram

While you can enable both browser caching and server caching, doing so can create problems. The reason for this is that if you update content on the server and want it served to your users, anyone who is caching content in the browser may not see those changes. For this reason it is generally best to pick one. Our speed tests showed a better first byte time and a slightly better document complete with server caching enabled. We will explain how to implement both browser caching and server caching below.

Server Caching

The following configuration is for apache servers. Locate the httpd.conf file on your server. It should be in your apache2/conf folder. We use bitnami so our full path is: /opt/bitnami/apache2/conf/httpd.conf.

Find this line and uncomment it:

LoadModule cache_module modules/mod_cache.so

Add this line underneath the above line:

LoadModule cache_disk_module modules/mod_cache_disk.so

Uncomment the following line:

LoadModule expires_module modules/mod_expires.so

At the bottom of the file add:

CacheEnable disk /
CacheRoot /webapps/cache/app1
CacheDefaultExpire 3600
CacheDisable /wp-admin

       ExpiresActive On
       ExpiresDefault "access plus 1 day"

Save your file and restart the server:

sudo /opt/bitnami/ctlscript.sh restart apache

You now have server caching enabled.

If you’d like to read more about server caching with Apache you can visit: http://publib.boulder.ibm.com/httpserv/manual70/mod/mod_cache.html

Browser Caching

Locate your .htaccess file which should be at the root of your WordPress installation. On our machines the path is /home/bitnami/apps/wordpress/htdocs/.htaccess

Add the following lines of code at the bottom of the file:

# 1 YEAR
<FilesMatch "\.(flv|ico|pdf|avi|mov|ppt|doc|mp3|wmv|wav|woff|woff2|ttf|eot)$">
Header set Cache-Control "max-age=31536000, public"
# 1 WEEK
<FilesMatch "\.(jpg|jpeg|png|gif|swf)$">
Header set Cache-Control "max-age=604800, public"
# 1 WEEK
<FilesMatch "\.(txt|xml|js|css)$">
Header set Cache-Control "max-age=604800, public"
# END WordPress

You now have browser caching enabled. To summarize, in this article we covered combining JS and CSS files, GZip compression, image compression and caching. By implementing these changes on our websites we can drastically improve our speed.

Our initial page insights score was 35 on mobile and 74 on desktop. Using the methods we discussed our mobile score improved to 80 on mobile and 94 on desktop. Our webpagetest.org speed results improved from 12.29 seconds for the document to load to 3.2 seconds.

If you have questions regarding the implementation of these techniques feel free to leave a comment or use our contact form.