Blog Performance Score Optimization

This article will be continuously updated, because optimization never ends.

This article lists the points that have been optimized so far. From the initial slight stutter on page load to eventually achieving a perfect score, I will show each step here.

Starting Point

The blog page optimization started with Chrome Performance.

As shown above, the page is blank for the first 550 ms; the time is spent loading JS. This segment is the Disqus script and the Google analytics script.

My optimization principle is: pre-load what must be loaded; lazy-load what does not have to be loaded and is not visible on the screen.

The optimization approach is simple: before the DOM is rendered, remove the long yellow rectangle in the figure above. That period is the time spent loading JS, which blocks page rendering. During page rendering, I do not load the Disqus script; it is lazy-loaded only on article pages, when the user clicks the button and the script is actually needed.

As for the Google analytics script, using the latest asynchronous version of gtag and adding the async flag is enough to resolve the issue. Of course, if you want this analytics code to be fully asynchronous, some server-side changes are required.

You can refer to these two articles: “Optimizing Asynchronous Loading of Google Analytics to Improve Your Website Speed” and “Using Google Analytics on the Server Side”.

If you also have a Baidu analytics script, move the code generated by Baidu analytics into the website and set type to test or another non-mime/type value. This way, the browser will not parse the script, while Baidu analytics can still pass verification:

<script type="text/test">
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "https://hm.baidu.com/hm.js?XXXX";
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(hm, s);
    })();
    </script>

For Baidu Analytics, you can refer to this article: “Deferred Asynchronous Loading of Analytics Code”.

Once you get the two sections above working properly, you’ll get the result shown below:

The effect is quite obvious: the homepage’s First Meaningful Paint (FMP, the time it takes for the main content to appear on the page) is now under 100 ms.

Benchmarking

Next is Chrome Audits. The results from my first run were not great. The lower scores were in Performance and Accessibility.

For Performance, the following metrics are measured:

  • First Meaningful Paint (FMP, the time it takes for the main content to appear on the page),
  • Important rendering time (the time it takes for the most important part of the page to finish rendering),
  • Time to Interactive (TTI, when the page layout has stabilized, key page fonts are visible, and the main thread can sufficiently handle user input — the basic timing marker is that the user can click and interact with the UI),
  • Input responsiveness, the time it takes for the interface to respond to user actions,
  • Speed Index, which measures how quickly page content is visually populated. The lower the score, the better,

The general definition of smoothness is:

Keep response time within 100 ms and the frame rate at 60 frames/second. Speed Index < 1250, time to interactive on 3G < 5s, critical file size < 170Kb(SpeedIndex < 1250, TTI < 5s on 3G, Critical file size budget < 170Kb)

The first 14–15 KB of HTML loaded is the most critical payload chunk — and it is the only part that can be delivered within the first round trip in a 1-second budget (assuming a 400 ms round-trip latency). In general, to achieve the targets above, we must operate within the critical file-size budget. The maximum compressed budget is 170 KB (0.8–1 MB decompressed), which can already take up to 1s (depending on the resource type) to parse and compile. Going slightly above this value is acceptable, but these values should be kept as low as possible.

Because this blog’s homepage uses a large number of images, image optimization is the biggest factor. In the screenshot above, Chrome gives three optimization suggestions for images:

  • Use JPEG 2000, JPEG XR, or WebP instead of PNG and JPEG wherever possible, which results in smaller image sizes.
  • The image dimensions fetched from the server should ideally already be scaled; avoid downloading oversized images.
  • Offscreen rendering. lazy-load images outside the viewport.

There are also several key areas to optimize for performance:

  • Reduce render-blocking stylesheets
  • Reduce unused CSS rules
  • Reduce render-blocking JS scripts

For Accessibility, the following metrics are easy to miss:

  • Elements should use the correct Attributes, for example adding the [alt] Attribute to <image>.
  • Elements should have a Discernible name. For example, add a Discernible name to <a>.
  • Form elements are missing associated labels.
  • Background and foreground colors must have sufficient color differentiation — a sufficient contrast ratio.

For the last point, I recommend installing the aXe extension in Chrome and using it to debug color contrast during development.

On the default page, there are also some requirements:

  • The <html> tag must include the [lang] attribute
  • Do not add [user-scalable="no"] inside the <meta name="viewport"> tag, and the [maximum-scale] attribute must be greater than 5

Finally, for SEO, there is a constraint on text: more than 75% of the text must be >= 16 px.

Optimization

The focus of optimization is, of course, images. There are three main aspects: use WebP as the image format, scale images to appropriate dimensions, and use lazy-load for images.

Here is a brief explanation of how lazy-load works: within the viewport, we can obtain the percentage of an element currently visible in the viewport. Based on that ratio, we can determine which element is about to be loaded next.

You can add a data-url attribute to the image tag to store the image’s real URL.

<img class="lazy" data-url="{{img_url feature_image}}" alt="">

When the user scrolls to the point where an image needs to be displayed, perform some processing and assign the final real image URL to the src attribute, so the browser will load the specified image.

For a more detailed explanation, see Google’s official article “IntersectionObserver’s Coming into View”.

Converting images to WebP requires using the API provided by Qiniu. If you use another CDN, you can also check whether it provides a similar usable API. If it does, things become much easier.

Qiniu provides several APIs for basic image processing. See this official document: “Qiniu Basic Image Processing API”.

The basic processing APIs above also include APIs for scaling images. Therefore, when fetching images from the CDN, we can resize the image to an appropriate dimension and convert it to WebP at the same time. The final image URL can then be assigned directly to src.

One thing to note, however, is that browsers on Apple platforms such as Safari and iOS do not support WebP. If you force-load WebP, the image quality degrades severely, the resolution is poor, and the actual rendered image will look very blurry. So before loading WebP, you need to check whether it is supported. If it is not supported, you need to fall back to JPEG 2000 or JPEG XR.

Other optimizations include optimizing JS and CSS files. Before deploying to production, make sure to minify these resource files. After minification, their size can become much smaller, reducing network transfer time. If JS files can be split into chunks and loaded on demand, the effect will be even better.

The key is to inspect each JavaScript dependency. Tools such as webpack-bundle-analyzer, Source Map Explorer, and Bundle Buddy can help you do this. Measure JavaScript parsing and compilation time. Etsy’s DeviceTiming is a small tool that lets your JavaScript measure parsing and execution time on any device or browser. Importantly, while file size matters, it is not the most important factor. Parsing and compilation time do not increase linearly with script size.

For JS file optimization, Webpack is the first recommendation.

Code-splitting is a Webpack feature that breaks your code into “chunks” loaded on demand. Not all JavaScript must be downloaded, parsed, and compiled. Once you identify split points in your code, Webpack handles those dependencies and output files. When the application sends requests, this basically ensures that the initial download is small enough and enables on-demand loading. In addition, consider using preload-webpack-plugin to obtain the code-splitting paths, then use <link rel="preload"> or <link rel="prefetch"> to hint to the browser to preload them.

If you cannot use Webpack, Rollup generally produces better output than Browserify. When using Rollup, it is recommended that you learn about Rollupify, which can convert ECMAScript 2015 modules into one large CommonJS module—because small modules can have surprisingly high performance overhead, depending on the choice of bundler and module loading system.

To ensure the browser starts rendering the page as quickly as possible, you typically collect all the CSS needed to render the first visible part of the page (called “critical CSS” or “above-the-fold CSS”) and inline it into the page’s <head>, thereby reducing round trips. Because the amount of data that can be exchanged during the slow-start phase is limited, the budget for critical CSS is roughly 14 KB.

Other Optimizations

1. PWA

No network performance optimization is faster than the local cache on the user’s machine. If your site runs on HTTPS, consider implementing a PWA. The goal is to use a service worker to cache static resources and store offline resources (even offline pages). It also teaches you how to retrieve data from the user’s device, which means you no longer need to request previously fetched data over the network.

A PWA provides a particularly large improvement when entering the page for the second time, because resources are loaded locally.

The author spent some effort on PWA and recommends reading this official article: “PWA Web App Manifest”.

Because of the nature of a blog, pages do not change much, so many resources can be cached. The author’s PWA caching strategy is to cache JS and CSS framework files, fonts, the Disqus framework, images, and the Baidu and Google analytics frameworks. As a result, when users load the site for the second time, 99% of resources come from cache, greatly improving load speed.

There is a well-written article about Service Workers that I recommend: “Using Service Workers”

2. HTTP/2

This also requires Nginx support.

server {
    listen 443 ssl http2;
}

The performance gains from HTTP/2 are substantial. I’ll analyze this topic in a future article in this series, so I won’t cover it in this optimization article.

If HTTP/2 is still not enabled after applying the configuration above, you can refer to this article: “Solving the Issue Where Nginx http2 Configuration Does Not Take Effect and Google Chrome Still Uses the HTTP/1.1 Protocol”

3. Enable gZip in Nginx

gZip can effectively reduce network transfer overhead. After it is enabled, it will consume a small amount of server CPU, but it can help improve frontend page performance. Here is the Nginx configuration from my own server:

http {
    include            mime.types;
    default_type       application/octet-stream;

    charset            UTF-8;

    sendfile           on;
    tcp_nopush         on;
    tcp_nodelay        on;

    keepalive_timeout  60;

    #... ...#

    gzip               on;
    gzip_vary          on;

    gzip_comp_level    6;
    gzip_buffers       16 8k;

    gzip_min_length    1000;
    gzip_proxied       any;
    gzip_disable       "msie6";

    gzip_http_version  1.0;

    gzip_types         text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;

    #... ...#

    include            /home/jerry/www/nginx_conf/*.conf;
}

4. Support TLS 1.3

There are plenty of tutorials online for this, so I won’t list specific links here.

The main steps are to first download the latest OpenSSL and Nginx, then compile OpenSSL, and finally compile Nginx. When compiling Nginx, include the OpenSSL you just built and the compilation options for TLS 1.3.

To enable TLS 1.3, you can refer to these two articles: “This site now supports TLS 1.3” and “This blog now supports TLS 1.3”.

5. Enable OCSP Stapling

server {
    ssl_session_cache        shared:SSL:10m;
    ssl_session_timeout      60m;

    ssl_session_tickets      on;

    ssl_stapling             on;
    ssl_stapling_verify      on;
    ssl_trusted_certificate  /xxx/full_chain.crt;

    resolver                 8.8.4.4 8.8.8.8  valid=300s;
    resolver_timeout         10s;
    ... ...
}

The purpose of TLS session resumption is to simplify the TLS handshake. There are two approaches: Session Cache and Session Ticket. Both store the Session from a previous handshake for use by subsequent connections. The difference is that Cache is stored on the server and consumes server-side resources, while Ticket is stored on the client and does not consume server-side resources. In addition, mainstream browsers currently support Session Cache, whereas support for Session Ticket is only average.

The first few lines starting with ssl_stapling are used to configure the OCSP stapling policy. Browsers may verify certificate validity online when establishing a TLS connection, which can block the TLS handshake and slow down overall performance. OCSP stapling is an optimization: the server can use it to encapsulate the certificate authority’s OCSP (Online Certificate Status Protocol) response in the certificate chain, allowing the browser to skip the online lookup. Having the server obtain the OCSP response is faster on the one hand (because servers generally have a better network environment), and on the other hand enables better caching.

Let’s Encrypt currently supports ECC certificates. After obtaining one, remember to enable OCSP Stapling as well. For the detailed steps, see this article: “Let’s Encrypt: A Free and Useful HTTPS Certificate”

After configuration, you need to verify whether OCSP Stapling is enabled. You can use the following command:

$ cd /var/www/ghost/ssl/

$ openssl ocsp -CAfile full_chained.pem -issuer intermediate.pem -cert chained.pem -no_nonce -text -url http://ocsp.int-x3.letsencrypt.org -header "HOST" "ocsp.int-x3.letsencrypt.org"

6. Support QUIC

This blog currently supports QUIC, but I haven’t noticed much of a performance improvement. Also, at the moment only Chrome supports QUIC, and the likelihood of it being enabled is extremely low; in practice, traffic still mostly goes over HTTP/2.

To support QUIC, you need to deploy caddy, a Go project, on the server. Since we already have Nginx occupying port 443, we need to set up port mapping, which can be done with Docker. In addition, the Nginx configuration also needs to add a header parameter:

add_header alt-svc 'quic=":443"; ma=2592000; v="39"';

7. Enable HSTS

To enable HSTS, the following requirements must first be met:

  • Have a valid certificate (if using a SHA-1 certificate, its expiration date must be earlier than 2016);
  • Redirect all HTTP traffic to HTTPS;
  • Ensure that HTTPS is enabled for all subdomains;
  • Send the HSTS response header: max-age must not be less than 18 weeks (10886400 seconds); The includeSubdomains parameter must be specified; The preload parameter must be specified;

Then Nginx needs to add the relevant security policy.


add_header  Strict-Transport-Security  "max-age=31536000";
add_header  X-Frame-Options  deny;
add_header  X-Content-Type-Options  nosniff;
add_header  Content-Security-Policy  "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://a.disquscdn.com; img-src 'self' data: https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://disqus.com";

ssl_certificate      /home/ssl/server.crt;
ssl_certificate_key  /home/ssl/server.key;
ssl_dhparam          /home/ssl/dhparams.pem;

ssl_ciphers          ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:DES-CBC3-SHA;

ssl_prefer_server_ciphers  on;

ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;

Finally, you can test it on ssllabs. I got an A+ score:

Conclusion

After completing all of the optimizations above, I ran the benchmark in Incognito mode with Chrome version 67.0.3396.99 (Stable) (64-bit)—I chose Incognito mode to reduce the impact of other extensions. Under excellent network conditions, the best result I got was as follows:

With no cache, FMP was 160ms and FI was 300ms.

With cache, both FMP and FI were 110ms, thanks to PWA caching.

But optimization is still far from finished. This article will continue to be updated.


Reference:

Optimize Google Analytics Async Loading to Improve Your Website Speed
Using Google Analytics on the Server Side Delayed Asynchronous Loading of Analytics Code
IntersectionObserver’s Coming into View
Qiniu Basic Image Processing API
PWA Web App Manifest
2018 Front-End Performance Optimization Checklist
Nginx Configuration: Security
Nginx Configuration: Complete Guide
Fixing Nginx Configuration Where HTTP/2 Does Not Take Effect and Chrome Still Uses HTTP/1.1
Using Service Workers
Make Your Ghost Blog Support Progressive Web Apps (PWA)
How This Site Enabled QUIC Support and Its Configuration
Let’s Encrypt: Free and Easy-to-Use HTTPS Certificates
This Site Now Supports TLS 1.3
This Blog Now Supports TLS 1.3
Starting from Why OCSP Stapling Cannot Be Enabled

GitHub Repo: Halfrost-Field

Follow: halfrost · GitHub

Source: https://halfrost.com/ghost_fast/