A recent in-depth performance analysis of WordPress core showed that loading translations had a significant hit on a site’s server response time. Given that more than half of all WordPress sites use a language other than English, the performance team identified this as an area worth looking into more closely. The team spent the last couple of months exploring this in more detail and the results are now shared in this blog post.
This is merely an analysis of the current i18n system in WordPress with some proposed under-the-hood performance improvements. No decisions have been made on any of these proposals.
Initial benchmarks showed that the median loading time for a localized site can be up to 50% slower than for non-localized sites, depending on which themes and plugins are being used. This was measured using both the wpp-research CLI tool and also a dedicated benchmark environment (as elaborated in the Comparison section towards the end).
The WordPress i18n system is based on gettext, which uses source .po (Portable Object) files and binary .mo (Machine Object) files for storing and loading translations. It is not using the C gettext API itself but a custom userland implementation that works without any external dependencies.
In addition to core itself, each plugin and theme has its own translation file, which has to be loaded and parsed on every request. Loading and parsing all these translation files is an expensive task.
In the past, various solutions have been discussed and explored to improve the i18n performance of WordPress. A non-exhaustive list:
.mo files, e.g. plain .php filesA more recent discussion touching on all of these solutions can be found over at the wordpress/performance repository. It’s a great way to get some context on this topic.
For this analysis, many of these solutions were looked at, focusing on their advantages and disadvantages. At the end of this post there is a comparison table with some much needed numbers as well, based on custom-built benchmarks.
Use a different file format for translations instead of .mo files to avoid the overhead of loading and parsing binary files.
With this solution, translations will be stored in plain .php files returning an associative array of translation strings. Whenever a .php file is available, it will be preferred over the .mo file, which is still used as a fallback. The rest of the architecture remains the same.
When a localized WordPress site downloads language packs from the translate.wordpress.org translation platform, it downloads .po and .mo files containing all the translations. This will be modified to include .php files. GlotPress, which the platform is built on, will be updated to support this new output format. Additionally, WordPress core itself could be modified to generate PHP files whenever they are missing.
In theory, nothing is faster in PHP than loading and executing another PHP file. .json, .ini, or .xml would all be much slower.
Proof of concepts using the PHP files can be found at swissspidy/wp-php-translation-files and swissspidy/ginger-mo.
.po to .mo).mo files, which is the main bottleneck.mo support could be deprecatedThe proof of concept using PHP files is in a very solid state already. There are also examples for changes to WP-CLI (PR) and GlotPress (PR). This makes it suitable for a feature project to expand testing with very little effort required. Even a core merge would be very straightforward in a relatively short time, potentially already in Q4 2023. The security aspect when using PHP files could be a potential blocker, so it’s important to loop in the WordPress security team and hosting providers early on.
More time is required to test other file formats and compare results.
Use the native gettext PHP extension written in C when available, instead of the custom built-in parser in WordPress.
WordPress has always used a custom MO file parser, because the native gettext extension is not necessarily available on the server. With this solution, the existing system is adapted to use the extension whenever available and falling back to the custom implementation if not.
This has been previously explored in #17268 and implemented in WP Performance Pack and Native Gettext. These implementations can serve as inspiration for the initial design. They all work similarly in that they symlink or copy the translation files to a new directory structure that is compatible with the gettext extension.
As of July 2023, around 66% of all localized WordPress sites have the gettext extension installed, according to information from the WordPress update requests.
pt_PT_ao90, de_DE_formal or roh might not even be supportedLC_MESSAGES and LANGUAGE), which might not be possible or cause conflicts on certain servers/sitesCheck out the code of WP Performance Pack and Native Gettext to get a better idea of the extension’s poor API.
While there are existing implementations that could be leveraged for this solution, further field testing is required to assess whether the extension actually works under all circumstances. Given the limitations around the poor API and requirements for installing locales, it does not seem like a viable solution at all.
Cache translations somehow to avoid expensive .mo parsing.
Cache translations either on disk, in the database, or the object cache to avoid expensive .mo file parsing on subsequent requests. This can be done in a generalized manner or also on a per-request basis to only load translations required for the current URL.
Many different caching strategies have been explored in various forms in the past, each with their own pros and cons. Some could even be combined. Defining the exact implementation requires further exploration and testing, which warrants its own exploration post.
.mo parsing potentially improves performance for future requestsGiven the existing solutions in the ecosystem, the engineering effort itself would not be too big, but the right caching implementation (e.g. disk cache or object cache) needs to be evaluated first.
However, the right caching strategy probably does not exist because of all the different hosting environments. Since it’s unrealistic for core to support multiple types of caching, this solution seems better suited for plugins rather than core.
Use lazily evaluated translation calls to reduce the number of function calls in certain cases, leading to improved performance.
The idea of lazily evaluated translation calls has been first discussed in #41305. It enables avoiding string-specific expensive translation lookups until the translations are actually needed, by passing around proxy objects.
In other words: beyond just-in-time loading of translation files (which WordPress already does), this would add just-in-time lookup of individual strings in the translations. Check out this proof of concept to get a better picture.
It can be integrated essentially in two ways, both of which are explained on the core ticket:
Gradual adoption would mean a multi-year effort to establish lazily evaluated translation calls, while enabling this by default is a significant backward compatibility break that could affect thousands of plugins and themes in the ecosystem. And since it does actually slow down performance in some cases, this solution is not a great candidate for implementation.
Refactor the existing MO parser in WordPress to be more performant.
Completely overhaul the existing MO translation file parser in WordPress with performance in mind. For example by using Ginger MO, WP Performance Pack, or other existing solutions as a base.
While for instance Altis DXP (Human Made) have actually replaced the existing MO parser with a custom-made PHP extension written in Rust, such an approach is obviously not feasible for core. The new solution needs to be written in userland PHP.
Initial testings with an updated fork of Ginger MO show some noticeable speedups and lower memory usage. It also supports multiple translation files per text domain and multiple locales loaded at once, which could prove beneficial for improving the locale switching functionality in WordPress core.
Besides that, plugins like WP Performance Pack and DynaMo have implemented partial lookups using the MO hash table or binary search, avoiding reading the whole file and storing it in memory. That slightly reduces memory usage and performance.
There already is a working proof of concept for this solution, but more testing is required to further refine it and improve its backward compatibility layer. With such an effort being an ideal candidate for a feature plugin, this could be achieved relatively quickly in a few months.
Split translation files from plugins and themes into smaller chunks to make loading them more efficient.
Depending on the project’s size, translation files can be quite big. That’s why WordPress itself uses separate translation files for the admin and everything else, so that not too many strings are unnecessarily loaded.
This strategy could be applied to plugins and themes as well. Either by allowing them to use multiple text domains (which would require developer education and changes to tooling), or by somehow doing this automatically (exact method TBD)
Further research is required to evaluate this properly.
At first glance, solution A (PHP translation files) is a relatively straightforward enhancement that maintains backward compatibility and shows promising improvements. However, it does not only require changes to core itself, but also to the translation platform. The security aspect remains a risk, although discussing it early on with stakeholders and gathering more testers would help mitigate it.
Leveraging the native gettext extension as in solution B shows stunning results, but the lack of availability and the non-ideal API are a concern. Still, it’s a progressive enhancement that cannot be ignored. Especially since it could pretty much eliminate the need for additional caching as in solution C.
Caching already loaded translations as in solution C does not eliminate the root cause of the i18n slowness, but can speed up subsequent requests. Unfortunately, persistent object caches or APCu are rather uncommon (though we do not have exact data on the former yet, see #58808), and implementing more complex types of caching (e.g. per-request caching) would require significant exploration effort before becoming a viable option.
Lazily evaluated translation calls (solution D) can shave time off translation calls in some situations, but overall actually decrease performance. While it could help solve some actual UX issues in core, the backward compatibility and adoption concerns make it even less of a suitable solution.
Existing plugins like Ginger MO and WP Performance Pack show that the existing MO parser in WordPress can be further improved (solution E).
Now to the most interesting part: the hard numbers!
These benchmarks are powered by a custom-built performance testing environment using @wordpress/env and Playwright. The environment has been configured with some additional plugins and the PHP extensions required for some of the solutions. Tests have been performed against the 6.3 RC by visiting the home page and the dashboard 30 times each and then using the median values.
You can find the exact setup in this wp-i18n-benchmarks GitHub repository.
| Locale | Scenario | Object Cache | Memory Usage | Total Load Time |
|---|---|---|---|---|
| en_US | Default | 15.60 MB | 133.58 ms | |
| de_DE | Default | 29.14 MB | 181.95 ms | |
| de_DE | Ginger MO (MO) | 19.24 MB | 159.18 ms | |
| de_DE | Ginger MO (PHP) | 16.98 MB | 138.14 ms | |
| de_DE | Ginger MO (JSON) | 19.24 MB | 153.39 ms | |
| de_DE | Native Gettext | 15.99 MB | 142.12 ms | |
| de_DE | DynaMo | 19.62 MB | 157.93 ms | |
| de_DE | Cache in APCu | 50.37 MB | 181.51 ms | |
| en_US | Default | 29.01 MB | 167.67 ms | |
| de_DE | Ginger MO (MO) | 16.85 MB | 127.97 ms | |
| de_DE | Ginger MO (JSON) | 15.86 MB | 129.19 ms | |
| de_DE | DynaMo | 50.30 MB | 170.19 ms | |
| de_DE | Cache in object cache | 29.07 MB | 173.19 ms |
| Locale | Scenario | Object Cache | Memory Usage | Total Load Time |
|---|---|---|---|---|
| en_US | Default | 15.35 MB | 120.79 ms | |
| de_DE | Default | 28.79 MB | 172.10 ms | |
| de_DE | Ginger MO (MO) | 18.85 MB | 145.68 ms | |
| de_DE | Ginger MO (PHP) | 16.56 MB | 124.73 ms | |
| de_DE | Ginger MO (JSON) | 18.84 MB | 140.78 ms | |
| de_DE | Native Gettext | 15.58 MB | 128.26 ms | |
| de_DE | DynaMo | 19.24 MB | 146.09 ms | |
| de_DE | Cache in APCu | 50.13 MB | 167.28 ms | |
| en_US | Default | 28.59 MB | 154.30 ms | |
| de_DE | Ginger MO (MO) | 16.37 MB | 112.94 ms | |
| de_DE | Ginger MO (JSON) | 15.38 MB | 115.11 ms | |
| de_DE | DynaMo | 49.99 MB | 151.82 ms | |
| de_DE | Cache in object cache | 28.65 MB | 156.36 ms |
| Locale | Scenario | Object Cache | Memory Usage | Total Load Time |
|---|---|---|---|---|
| en_US | Default | 15.42 MB | 139.83 ms | |
| de_DE | Default | 31.92 MB | 187.76 ms | |
| de_DE | Ginger MO (MO) | 20.07 MB | 164.94 ms | |
| de_DE | Ginger MO (PHP) | 17.09 MB | 139.66 ms | |
| de_DE | Ginger MO (JSON) | 20.06 MB | 160.87 ms | |
| de_DE | Native Gettext | 15.95 MB | 143.43 ms | |
| de_DE | DynaMo | 20.58 MB | 166.79 ms | |
| de_DE | Cache in APCu | 58.13 MB | 190.38 ms | |
| en_US | Default | 31.84 MB | 164.26 ms | |
| de_DE | Ginger MO (MO) | 17.01 MB | 118.52 ms | |
| de_DE | Ginger MO (JSON) | 15.87 MB | 120.01 ms | |
| de_DE | DynaMo | 58.07 MB | 162.41 ms | |
| de_DE | Cache in object cache | 31.86 MB | 164.28 ms |
Finding the right path forward means weighing all the pros and cons of each solution and looking at both horizontal and vertical impact, i.e. how much faster can i18n be made for how many sites.
When looking at all these factors, it appears that a revamped translations parser (solution E) could bring the most significant improvements to all localized WordPress sites. Especially when combined with a new PHP translation file format (solution A), which Ginger MO supports, the i18n overhead becomes negligible. Of course the same risks associated with introducing a new format apply.
On top of that, a revamped i18n library like Ginger MO could also be combined with other solutions such as caching or dynamic MO loading to potentially gain further improvements. However, those routes have yet to be explored.
The WordPress performance team wants to further dive into this topic and test some of the above solutions (and combinations thereof) on a wider scale through efforts like the Performance Lab feature project. We are looking forward to hearing your feedback on this analysis and welcome any additional comments, insights, and tinkering.
Thank you to @flixos90, @westonruter, @joemcgill, @spacedmonkey, and @adamsilverstein for reviewing and helping with this post. Thank you to @nbachiyski, @ocean90, @akirk, @rmccue, @dd32 for providing valuable insights and context.
With so many website hosting choices, it can be difficult to decide which type of WordPress hosting…
WordCamp Asia 2026 brought the global WordPress community to Mumbai, India, from April 9–11, gathering…
WordCamp Asia 2026 will be available to watch live across three days of streaming, making…
April 9-11, 2026 | Jio World Convention Centre, Mumbai, India WordCamp Asia 2026 brings the…
The second Release Candidate (“RC2”) for WordPress 7.0 is ready for download and testing! This…
When WP Engine acquired WPackagist on March 12, the WordPress developer community faced a familiar…