Back
Featured image of post Speeding up apps.kde.org

Speeding up apps.kde.org

Apps.kde.org is a great website listing all the KDE applications and their addons. Under the hood, it’s using AppStream, a standard for adding metadata to Linux applications. The Linux application managers (Discover, GNOME Softwares, …) are displaying them, so they stay up to date and are translated. There was only one problem, apps.kde.org has always been a bit slow to load. It is a problem since slow websites tend to be less visible on Google search results and for the users there aren’t a good browsing experience.

There were many reasons why it was slow. We designed it earlier in a way that on each page load the backend load the data from json files. On the individual application pages, we only need to read one json file but on the homepage every single JSON files was loaded. The Linux kernel is probably caching every JSON file in the memory so that the IO load wasn’t that bad, but parsing the JSON files still needed to be done on each page load.

Using PHP and a Symfony (a big PHP framework) for doing the rooting, templating and internalization (loading additional mo files) of the website adds an overhead. The final result was that the load time for just one html files, was between 300ms and 500ms.

That doens’t sounds like a big deal, but it is one. Rendering a page takes more than just downloading one HTML file, the browser needs to load the other assets (images, CSS and javascript files); it needs to parse the HTML and CSS files and compute the layout. These operations are done in parallel, but the initial loading of the HTML files blocks every other operation. Also, these 300ms loading times can be a lot longer on bad internet connections.

As often to solve the performance problem, my usual solution is to port the websites to static site generators. This isn’t always a solution that can be used but in this case, a generator was already generating the data as JSON files twice a day, so there was no technical reasons for dynamically generating the pages. I choose Hugo, because it is my preferred static site generator, it supports internalization and we have a shared theme for it in KDE so most of the layouts, CSS files and translations can be shared with the other KDE websites.

Porting to Hugo was not difficult, I was already using a templating engine (TWIG) in the old websites and porting to the Hugo templating engine wasn’t complicated. I also wrote a small python script, converting the json files containing the AppStream metadata to markdown files in a way that Hugo can read it. The side-effect is that this makes the generation slower. Generating apps.kde.org is really pushing Hugo to its limits and even if it is one of the fastest static site generator, it now takes 20s for hugo to generate all the pages. This can be explained by the fact that it generates 240 pages in almost 30 languages.

Fortunately while making it slower to generate in the CI, it made it faster to load for the users. It moved the loading time from ~300-500ms to under 100ms. This is already a nice speedup, but wasn’t enough for me. The second step was to improve the rendering time of homepage. Looking at Google Lighthouse, one of the reasons why the homepage was still slow was the large amount of DOM elements. It was over 2000, so I used some tricks to decrease the amount:

  • First one was to remove all the addons from the homepage, they aren’t really helpful for the visitors to know about them and 15% of the content was about them. Instead I moved these addons information to the applications they extend. This makes it easier to get a list of addons for each application without adding too much clutter to the homepage.
  • The second trick was to port the application grid from a CSS flex element to grid. Flex has the disadvantage that it doesn’t allow to specify a gab between the items, so to create spacing, I needed to wrap every element in a div with padding. (Flex support gap but only on Firefox for now). display: grid also has more advantages, for example I don’t need to specify how many elements I want per row on each screen sizes but instead can specify the width of an item and its spacing will be automatically adjusted. Here is the new css rule for …
.application-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  align-items: top;
  grid-gap: 1em;
}

… and the corresponding changes applied to the HTML. I also merged the two <a> element together.

-    <div class="application-list row align-items-stretch">
+    <div class="application-list">
       {{ $category := .Params.categoryName }}
       {{ range where (where site.RegularPages "Section" "applications") ".Params.appType" "!=" "addon" }}
         {{ if and (eq $category .Params.MainCategory) (ne .Params.appType "addon") }}
-          <div class="app text-center col-12 col-sm-6 col-md-4 col-lg-3 p-2">
-            <div class="p-3 h-100">
-              <div aria-hidden="true">
-                <a href="{{ .Permalink }}">
-                  <img width="48" height="48" src="/app-icons/{{ .Params.icon }}"
-                       loading="lazy"
-                       alt="{{ .Params.Name }}" title="{{ .Params.name }}"/>
-                </a>
-              </div>
-              <a><h3>{{ .Params.name }}</h3></a>
-              <p>{{ .Params.GenericName }}</p>
-            </div>
+          <div class="app text-center">
+            <a href="{{ .Permalink }}" class="d-flex flex-column">
+              <img width="48" height="48" aria-hidden=true class="icon" src="/app-icons/{{ .Params.icon }}"
+                   loading="lazy"
+                   alt="{{ .Params.Name }}" title="{{ .Params.name }}"/>
+              <h3>{{ .Params.name }}</h3>
+            </a>
+            <p>{{ .Params.GenericName }}</p>
           </div>
         {{ end }}
       {{ end }}
     </div>

This change removed 2 DOM elements per item, again reducing the amount of items that the browser need to download, parse and render by another 5%.

I’m actually wondering if using display: grid is also easier for the browser engine to render, since the improvement from my impression seemed faster than just 5%.

The end result is that the homepage get a score of 96/100 on Google Lighthouse, this is a big improvement from 56/100 we were getting a few weeks ago. Hopefully Google PageRank algorithm also like the change.

There is still a few improvements possibilities, mostly slimming down the CSS files. That should be doable thanks to the fact we can use Hugo to ship a customized version of the scss code shared with the other websites with less imported modules and just the one required for apps.kde.org.

Comments

With an account on the Fediverse or Mastodon, you can respond to this post. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.

Learn how this is implemented here.