I18N Performance Analysis

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.  

Contents hide

Context

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:

  • Use a more lightweight MO parser
  • Improve translation lookups by using the hash map in MO files (e.g. with DynaMo)
  • Caching translations in the object cache
  • Caching translations in APCu (an in-memory key-value store for PHP)
  • Other more elaborated forms of caching (e.g. per request)
  • Using the native PHP gettext extension
  • Use a custom PHP extension to handle the MO file parsing)
  • Using lazily evaluated translation calls (see #41305 for details)
  • Using a different file format than .mo files, e.g. plain .php files

A 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.

Solutions

Solution A: Use different file format

Use a different file format for translations instead of .mo files to avoid the overhead of loading and parsing binary files.

Design considerations

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.

Benefits

  • Initial benchmarks show consistent significant performance improvements
  • Relatively trivial to implement
  • Maintains backward compatibility thanks to graceful fallback
  • Makes it easier for users to inspect and change translations (no need to compile .po to .mo)
  • Avoids loading and parsing binary .mo files, which is the main bottleneck
  • Lets PHP store translations in OPcache for an additional performance benefit
  • Battle-tested approach in the PHP ecosystem (for example in Laravel)

Caveats and risks

  • Requires not only changes to WordPress core, but also tools like GlotPress and WP-CLI
  • Adds maintenance overhead by introducing a new file format on top of the existing one
    • As shown by the proof of concept, the overhead is minimal
    • In the long term, .mo support could be deprecated
  • Security considerations due to essentially executing remotely fetched PHP files
    • Not really different from downloading plugins/themes from WordPress.org
    • WordPress considers translations to be trusted
    • Hosting providers could be blocking PHP execution in wp-content/languages
    • Could potentially use checksum verifications or static analysis at install time to detect anomalies

Effort and timeline

The 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.

Solution B: Native gettext extension

Use the native gettext PHP extension written in C when available, instead of the custom built-in parser in WordPress.

Design considerations

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.

Benefits

  • Significant performance improvements for eligible sites
    • Initial benchmarks show that loading time and memory usage basically do not differ from non-localized sites

Caveats and risks

  • The gettext extension is not commonly available
    • Smaller incentive to implement and lower impact overall
  • Requires locales to be installed on the server
    • Servers rarely have many installed locales
      • Locales often need to be compiled first and take up a lot of space
      • WordPress on the other hand supports over 200 locales
    • Potential clashes with the custom locales WordPress supports
      • For example, locales like pt_PT_ao90, de_DE_formal or roh might not even be supported
    • Outreach to hosting providers would be necessary
  • Adds maintenance overhead by essentially adding a second gettext implementation
  • Poor API
    • Requires setting environment variables (such as LC_MESSAGES and LANGUAGE), which might not be possible or cause conflicts on certain servers/sites
  • Requires symlinks or hard file copies
    • Symlinks might not be possible on the server; copying all translation files means doubling disk usage
  • Translation files are cached by PHP, thus any translation change requires restarting the web server
    • There are workarounds such as cache busting using random file names or fstat, however they might not work on all environments
  • Has not been tested on a wider scale, despite being discussed for years

Check out the code of WP Performance Pack and Native Gettext to get a better idea of the extension’s poor API.

Effort and timeline

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.

Solution C: Cache translations

Cache translations somehow to avoid expensive .mo parsing.

Design considerations

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.

Benefits

  • Caching translations after one time .mo parsing potentially improves performance for future requests

Caveats and risks

  • Caching using persistent object cache (e.g. Memcached, Redis) or APCu:
    • Not available on most sites, making this not an ideal solution
      • Availability according to data from WordPress update requests:
        • Memcached: ~25%
        • Redis: ~25%
        • APCu: ~6%
    • Can potentially significantly increase cache size or exceed cache key limits
  • Database caching:
    • Moves the problem from disk reads to database reads
    • Can potentially significantly increase database size
    • Alternatively, use sqlite as a cache backend
      • Untested approach
      • Available on around 90% of sites
  • Disk caching:
    • Not always possible, depending on server environment
    • Still causes file reads, only with fewer or other files
  • Multiple cache groups (e.g. per-request or frontend/admin split)
    • Smarter cache logic to only load translations that are needed for the majority of requests
    • Can potentially significantly increase cache size
    • Unlikely that different requests use very different translations
  • Cache retrieval adds overhead
    • Exact performance gains depend on implementation method and need to be measured first
    • No performance gains with cold cache
    • Cache invalidation logic TBD

Effort and timeline

Given 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.

Solution D: Lazily evaluated translation calls

Use lazily evaluated translation calls to reduce the number of function calls in certain cases, leading to improved performance.

Design considerations

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:

  1. Change all translation calls to be lazily evaluated by default
  2. Make this opt-in, either with new function arguments or new functions altogether

Benefits

  • Reduces the number of translation lookups, in some scenarios drastically
    • On a regular home page request there are ~60% less translation calls, saving around ~10ms (as measured by XHProf)
  • As a side effect, solves UX issues such as #38643

Caveats and risks

  • Depending on implementation this either breaks backward compatibility or risks not gaining enough adoption
    • Documentation, tooling, and developer education can help mitigate this to a certain extent
    • Adoption could be done gradually, e.g. starting with an opt-in approach and eventually making it the default
  • Likely will not have a significant impact on typical frontend page loads, as it’s mostly useful for areas like the REST API schema output, where a lot of translation calls are made without actually using the translations
    • Needs analysis in more scenarios to measure impact
    • The REST API schema already has a workaround by using a cache in a static variable
  • Does not improve situation for actually loading translation files
  • Initial testing shows that this actually hurts performance due to the additional thousands of proxy objects being created

Effort and timeline

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.

Solution E: Optimize/Rewrite existing MO parser

Refactor the existing MO parser in WordPress to be more performant.

Design considerations

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.

Benefits

  • Can be used without necessarily introducing another file format
  • Opens up potential performance enhancements in other areas, i.e. locale switching
  • Mostly maintains backward compatibility

Caveats and risks

  • Still a risk of breaking backward compatibility

Effort and timeline

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.

Solution F: Splitting up translation files

Split translation files from plugins and themes into smaller chunks to make loading them more efficient.

Design considerations

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)

Benefits

  • Faster loading times due to loading smaller files

Caveats and risks

  • Risk of breaking backward compatibility
  • Opt-in approach requires tooling and distribution changes and risks slow adoption

Effort and timeline

Further research is required to evaluate this properly.

Comparison

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).

Benchmarks

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.

Block Theme

LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.60 MB133.58 ms
de_DEDefault29.14 MB181.95 ms
de_DEGinger MO (MO)19.24 MB159.18 ms
de_DEGinger MO (PHP)16.98 MB138.14 ms
de_DEGinger MO (JSON)19.24 MB153.39 ms
de_DENative Gettext15.99 MB142.12 ms
de_DEDynaMo19.62 MB157.93 ms
de_DECache in APCu50.37 MB181.51 ms
en_USDefault29.01 MB167.67 ms
de_DEGinger MO (MO)16.85 MB127.97 ms
de_DEGinger MO (JSON)15.86 MB129.19 ms
de_DEDynaMo50.30 MB170.19 ms
de_DECache in object cache29.07 MB173.19 ms
Benchmarks using the Twenty Twenty-Three block theme

Classic Theme

LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.35 MB120.79 ms
de_DEDefault28.79 MB172.10 ms
de_DEGinger MO (MO)18.85 MB145.68 ms
de_DEGinger MO (PHP)16.56 MB124.73 ms
de_DEGinger MO (JSON)18.84 MB140.78 ms
de_DENative Gettext15.58 MB128.26 ms
de_DEDynaMo19.24 MB146.09 ms
de_DECache in APCu50.13 MB167.28 ms
en_USDefault28.59 MB154.30 ms
de_DEGinger MO (MO)16.37 MB112.94 ms
de_DEGinger MO (JSON)15.38 MB115.11 ms
de_DEDynaMo49.99 MB151.82 ms
de_DECache in object cache28.65 MB156.36 ms
Benchmarks using the Twenty Twenty-One classic theme

Admin

LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.42 MB139.83 ms
de_DEDefault31.92 MB187.76 ms
de_DEGinger MO (MO)20.07 MB164.94 ms
de_DEGinger MO (PHP)17.09 MB139.66 ms
de_DEGinger MO (JSON)20.06 MB160.87 ms
de_DENative Gettext15.95 MB143.43 ms
de_DEDynaMo20.58 MB166.79 ms
de_DECache in APCu58.13 MB190.38 ms
en_USDefault31.84 MB164.26 ms
de_DEGinger MO (MO)17.01 MB118.52 ms
de_DEGinger MO (JSON)15.87 MB120.01 ms
de_DEDynaMo58.07 MB162.41 ms
de_DECache in object cache31.86 MB164.28 ms
Benchmarks visiting the WordPress admin

Conclusion

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.

Next steps

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.

#core, #i18n, #performance

nnnn

Context

nnnn

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).

nnnn

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.

nnnn

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.

nnnn

In the past, various solutions have been discussed and explored to improve the i18n performance of WordPress. A non-exhaustive list:

nnnn
    n
  • Use a more lightweight MO parser
  • nnnn
  • Improve translation lookups by using the hash map in MO files (e.g. with DynaMo)
  • nnnn
  • Caching translations in the object cache
  • nnnn
  • Caching translations in APCu (an in-memory key-value store for PHP)
  • nnnn
  • Other more elaborated forms of caching (e.g. per request)
  • nnnn
  • Using the native PHP gettext extension
  • nnnn
  • Use a custom PHP extension to handle the MO file parsing)
  • nnnn
  • Using lazily evaluated translation calls (see #41305 for details)
  • nnnn
  • Using a different file format than .mo files, e.g. plain .php files
  • n
nnnn

A more recent discussion touching on all of these solutions can be found over at the wordpress/performance repository. Itu2019s a great way to get some context on this topic.

nnnn

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.

nnnn

Solutions

nnnn

Solution A: Use different file format

nnnn

Use a different file format for translations instead of .mo files to avoid the overhead of loading and parsing binary files.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

In theory, nothing is faster in PHP than loading and executing another PHP file. .json, .ini, or .xml would all be much slower.

nnnn

Proof of concepts using the PHP files can be found at swissspidy/wp-php-translation-files and swissspidy/ginger-mo.

nnnn

Benefits

nnnn
    n
  • Initial benchmarks show consistent significant performance improvements
  • nnnn
  • Relatively trivial to implement
  • nnnn
  • Maintains backward compatibility thanks to graceful fallback
  • nnnn
  • Makes it easier for users to inspect and change translations (no need to compile .po to .mo)
  • nnnn
  • Avoids loading and parsing binary .mo files, which is the main bottleneck
  • nnnn
  • Lets PHP store translations in OPcache for an additional performance benefit
  • nnnn
  • Battle-tested approach in the PHP ecosystem (for example in Laravel)
  • n
nnnn

Caveats and risks

nnnn
    n
  • Requires not only changes to WordPress core, but also tools like GlotPress and WP-CLI
  • nnnn
  • Adds maintenance overhead by introducing a new file format on top of the existing onen
      n
    • As shown by the proof of concept, the overhead is minimal
    • nnnn
    • In the long term, .mo support could be deprecated
    • n
    n
  • nnnn
  • Security considerations due to essentially executing remotely fetched PHP filesn
      n
    • Not really different from downloading plugins/themes from WordPress.org
    • nnnn
    • WordPress considers translations to be trusted
    • nnnn
    • Hosting providers could be blocking PHP execution in wp-content/languages
    • nnnn
    • Could potentially use checksum verifications or static analysis at install time to detect anomalies
    • n
    n
  • n
nnnn

Effort and timeline

nnnn

The 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 itu2019s important to loop in the WordPress security team and hosting providers early on.

nnnn

More time is required to test other file formats and compare results.

nnnn

Solution B: Native gettext extension

nnnn

Use the native gettext PHP extension written in C when available, instead of the custom built-in parser in WordPress.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

As of July 2023, around 66% of all localized WordPress sites have the gettext extension installed, according to information from the WordPress update requests.

nnnn

Benefits

nnnn
    n
  • Significant performance improvements for eligible sitesn
      n
    • Initial benchmarks show that loading time and memory usage basically do not differ from non-localized sites
    • n
    n
  • n
nnnn

Caveats and risks

nnnn
    n
  • The gettext extension is not commonly availablen
      n
    • Smaller incentive to implement and lower impact overall
    • n
    n
  • nnnn
  • Requires locales to be installed on the servern
      n
    • Servers rarely have many installed localesn
        n
      • Locales often need to be compiled first and take up a lot of space
      • nnnn
      • WordPress on the other hand supports over 200 locales
      • n
      n
    • nnnn
    • Potential clashes with the custom locales WordPress supportsn
        n
      • For example, locales like pt_PT_ao90, de_DE_formal or roh might not even be supported
      • n
      n
    • nnnn
    • Outreach to hosting providers would be necessary
    • n
    n
  • nnnn
  • Adds maintenance overhead by essentially adding a second gettext implementation
  • nnnn
  • Poor APIn
      n
    • Requires setting environment variables (such as LC_MESSAGES and LANGUAGE), which might not be possible or cause conflicts on certain servers/sites
    • n
    n
  • nnnn
  • Requires symlinks or hard file copiesn
      n
    • Symlinks might not be possible on the server; copying all translation files means doubling disk usage
    • n
    n
  • nnnn
  • Translation files are cached by PHP, thus any translation change requires restarting the web servern
      n
    • There are workarounds such as cache busting using random file names or fstat, however they might not work on all environments
    • n
    n
  • nnnn
  • Has not been tested on a wider scale, despite being discussed for years
  • n
nnnn

Check out the code of WP Performance Pack and Native Gettext to get a better idea of the extensionu2019s poor API.

nnnn

Effort and timeline

nnnn

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.

nnnn

Solution C: Cache translations

nnnn

Cache translations somehow to avoid expensive .mo parsing.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

Benefits

nnnn
    n
  • Caching translations after one time .mo parsing potentially improves performance for future requests
  • n
nnnn

Caveats and risks

nnnn
    n
  • Caching using persistent object cache (e.g. Memcached, Redis) or APCu:n
      n
    • Not available on most sites, making this not an ideal solutionn
        n
      • Availability according to data from WordPress update requests:n
          n
        • Memcached: ~25%
        • nnnn
        • Redis: ~25%
        • nnnn
        • APCu: ~6%
        • n
        n
      • n
      n
    • nnnn
    • Can potentially significantly increase cache size or exceed cache key limits
    • n
    n
  • nnnn
  • Database caching:n
      n
    • Moves the problem from disk reads to database reads
    • nnnn
    • Can potentially significantly increase database size
    • nnnn
    • Alternatively, use sqlite as a cache backendn
        n
      • Untested approach
      • nnnn
      • Available on around 90% of sites
      • n
      n
    • n
    n
  • nnnn
  • Disk caching:n
      n
    • Not always possible, depending on server environment
    • nnnn
    • Still causes file reads, only with fewer or other files
    • n
    n
  • nnnn
  • Multiple cache groups (e.g. per-request or frontend/admin split)n
      n
    • Smarter cache logic to only load translations that are needed for the majority of requests
    • nnnn
    • Can potentially significantly increase cache size
    • nnnn
    • Unlikely that different requests use very different translations
    • n
    n
  • nnnn
  • Cache retrieval adds overheadn
      n
    • Exact performance gains depend on implementation method and need to be measured first
    • nnnn
    • No performance gains with cold cache
    • nnnn
    • Cache invalidation logic TBD
    • n
    n
  • n
nnnn

Effort and timeline

nnnn

Given 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.

nnnn

However, the right caching strategy probably does not exist because of all the different hosting environments. Since itu2019s unrealistic for core to support multiple types of caching, this solution seems better suited for plugins rather than core.

nnnn

Solution D: Lazily evaluated translation calls

nnnn

Use lazily evaluated translation calls to reduce the number of function calls in certain cases, leading to improved performance.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

It can be integrated essentially in two ways, both of which are explained on the core ticket:

nnnn
    n
  1. Change all translation calls to be lazily evaluated by default
  2. nnnn
  3. Make this opt-in, either with new function arguments or new functions altogether
  4. n
nnnn

Benefits

nnnn
    n
  • Reduces the number of translation lookups, in some scenarios drasticallyn
      n
    • On a regular home page request there are ~60% less translation calls, saving around ~10ms (as measured by XHProf)
    • n
    n
  • nnnn
  • As a side effect, solves UX issues such as #38643
  • n
nnnn

Caveats and risks

nnnn
    n
  • Depending on implementation this either breaks backward compatibility or risks not gaining enough adoptionn
      n
    • Documentation, tooling, and developer education can help mitigate this to a certain extent
    • nnnn
    • Adoption could be done gradually, e.g. starting with an opt-in approach and eventually making it the default
    • n
    n
  • nnnn
  • Likely will not have a significant impact on typical frontend page loads, as itu2019s mostly useful for areas like the REST API schema output, where a lot of translation calls are made without actually using the translationsn
      n
    • Needs analysis in more scenarios to measure impact
    • nnnn
    • The REST API schema already has a workaround by using a cache in a static variable
    • n
    n
  • nnnn
  • Does not improve situation for actually loading translation files
  • nnnn
  • Initial testing shows that this actually hurts performance due to the additional thousands of proxy objects being created
  • n
nnnn

Effort and timeline

nnnn

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.

nnnn

Solution E: Optimize/Rewrite existing MO parser

nnnn

Refactor the existing MO parser in WordPress to be more performant.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

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.

nnnn

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.

nnnn

Benefits

nnnn
    n
  • Can be used without necessarily introducing another file format
  • nnnn
  • Opens up potential performance enhancements in other areas, i.e. locale switching
  • nnnn
  • Mostly maintains backward compatibility
  • n
nnnn

Caveats and risks

nnnn
    n
  • Still a risk of breaking backward compatibility
  • n
nnnn

Effort and timeline

nnnn

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.

nnnn

Solution F: Splitting up translation files

nnnn

Split translation files from plugins and themes into smaller chunks to make loading them more efficient.

nnnn

Design considerations

nnnn

Depending on the projectu2019s size, translation files can be quite big. Thatu2019s why WordPress itself uses separate translation files for the admin and everything else, so that not too many strings are unnecessarily loaded.

nnnn

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)

nnnn

Benefits

nnnn
    n
  • Faster loading times due to loading smaller files
  • n
nnnn

Caveats and risks

nnnn
    n
  • Risk of breaking backward compatibility
  • nnnn
  • Opt-in approach requires tooling and distribution changes and risks slow adoption
  • n
nnnn

Effort and timeline

nnnn

Further research is required to evaluate this properly.

nnnn

Comparison

nnnn

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.

nnnn

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, itu2019s a progressive enhancement that cannot be ignored. Especially since it could pretty much eliminate the need for additional caching as in solution C.

nnnn

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.

nnnn

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.

nnnn

Existing plugins like Ginger MO and WP Performance Pack show that the existing MO parser in WordPress can be further improved (solution E).

nnnn

Benchmarks

nnnn

Now to the most interesting part: the hard numbers!

nnnn

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.

nnnn

You can find the exact setup in this wp-i18n-benchmarks GitHub repository.

nnnn

Block Theme

nnnn
LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.60 MB133.58 ms
de_DEDefault29.14 MB181.95 ms
de_DEGinger MO (MO)19.24 MB159.18 ms
de_DEGinger MO (PHP)16.98 MB138.14 ms
de_DEGinger MO (JSON)19.24 MB153.39 ms
de_DENative Gettext15.99 MB142.12 ms
de_DEDynaMo19.62 MB157.93 ms
de_DECache in APCu50.37 MB181.51 ms
en_USDefaultu270515.67 MB121.53 ms
de_DEDefaultu270529.01 MB167.67 ms
de_DEGinger MO (MO)u270519.11 MB147.19 ms
de_DEGinger MO (PHP)u270516.85 MB127.97 ms
de_DEGinger MO (JSON)u270519.11 MB144.43 ms
de_DENative Gettextu270515.86 MB129.19 ms
de_DEDynaMou270518.57 MB133.46 ms
de_DECache in APCuu270550.30 MB170.19 ms
de_DECache in object cacheu270529.07 MB173.19 ms
Benchmarks using the Twenty Twenty-Three block theme
nnnn

Classic Theme

nnnn
LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.35 MB120.79 ms
de_DEDefault28.79 MB172.10 ms
de_DEGinger MO (MO)18.85 MB145.68 ms
de_DEGinger MO (PHP)16.56 MB124.73 ms
de_DEGinger MO (JSON)18.84 MB140.78 ms
de_DENative Gettext15.58 MB128.26 ms
de_DEDynaMo19.24 MB146.09 ms
de_DECache in APCu50.13 MB167.28 ms
en_USDefaultu270515.19 MB107.26 ms
de_DEDefaultu270528.59 MB154.30 ms
de_DEGinger MO (MO)u270518.64 MB133.21 ms
de_DEGinger MO (PHP)u270516.37 MB112.94 ms
de_DEGinger MO (JSON)u270518.64 MB128.94 ms
de_DENative Gettextu270515.38 MB115.11 ms
de_DEDynaMou270518.10 MB120.72 ms
de_DECache in APCuu270549.99 MB151.82 ms
de_DECache in object cacheu270528.65 MB156.36 ms
Benchmarks using the Twenty Twenty-One classic theme
nnnn

Admin

nnnn
LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.42 MB139.83 ms
de_DEDefault31.92 MB187.76 ms
de_DEGinger MO (MO)20.07 MB164.94 ms
de_DEGinger MO (PHP)17.09 MB139.66 ms
de_DEGinger MO (JSON)20.06 MB160.87 ms
de_DENative Gettext15.95 MB143.43 ms
de_DEDynaMo20.58 MB166.79 ms
de_DECache in APCu58.13 MB190.38 ms
en_USDefaultu270515.66 MB112.69 ms
de_DEDefaultu270531.84 MB164.26 ms
de_DEGinger MO (MO)u270519.99 MB140.70 ms
de_DEGinger MO (PHP)u270517.01 MB118.52 ms
de_DEGinger MO (JSON)u270519.98 MB138.49 ms
de_DENative Gettextu270515.87 MB120.01 ms
de_DEDynaMou270519.73 MB120.26 ms
de_DECache in APCuu270558.07 MB162.41 ms
de_DECache in object cacheu270531.86 MB164.28 ms
Benchmarks visiting the WordPress admin
nnnn

Conclusion

nnnn

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.

nnnn

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.

nnnn

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.

nnnn

Next steps

nnnn

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.

nnnn
nnnn

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.

nnn#core, #i18n, #performance","contentFiltered":"

A recent in-depth performance analysis of WordPress core showed that loading translations had a significant hit on a siteu2019s 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.

nnnn
n

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.u00a0u00a0

n
nnnn

Context

nnnn

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).

nnnn

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.

nnnn

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.

nnnn

In the past, various solutions have been discussed and explored to improve the i18n performance of WordPress. A non-exhaustive list:

nnnn
  • Use a more lightweight MO parser
  • nnnn
  • Improve translation lookups by using the hash map in MO files (e.g. with DynaMo)
  • nnnn
  • Caching translations in the object cache
  • nnnn
  • Caching translations in APCu (an in-memory key-value store for PHP)
  • nnnn
  • Other more elaborated forms of caching (e.g. per request)
  • nnnn
  • Using the native PHP gettext extension
  • nnnn
  • Use a custom PHP extension to handle the MO file parsing)
  • nnnn
  • Using lazily evaluated translation calls (see #41305 for details)
  • nnnn
  • Using a different file format than .mo files, e.g. plain .php files
  • n

A more recent discussion touching on all of these solutions can be found over at the wordpress/performance repository. Itu2019s a great way to get some context on this topic.

nnnn

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.

nnnn

Solutions

nnnn

Solution A: Use different file format

nnnn

Use a different file format for translations instead of .mo files to avoid the overhead of loading and parsing binary files.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

In theory, nothing is faster in PHP than loading and executing another PHP file. .json, .ini, or .xml would all be much slower.

nnnn

Proof of concepts using the PHP files can be found at swissspidy/wp-php-translation-files and swissspidy/ginger-mo.

nnnn

Benefits

nnnn
  • Initial benchmarks show consistent significant performance improvements
  • nnnn
  • Relatively trivial to implement
  • nnnn
  • Maintains backward compatibility thanks to graceful fallback
  • nnnn
  • Makes it easier for users to inspect and change translations (no need to compile .po to .mo)
  • nnnn
  • Avoids loading and parsing binary .mo files, which is the main bottleneck
  • nnnn
  • Lets PHP store translations in OPcache for an additional performance benefit
  • nnnn
  • Battle-tested approach in the PHP ecosystem (for example in Laravel)
  • n

Caveats and risks

nnnn
  • Requires not only changes to WordPress core, but also tools like GlotPress and WP-CLI
  • nnnn
  • Adds maintenance overhead by introducing a new file format on top of the existing onen
    • As shown by the proof of concept, the overhead is minimal
    • nnnn
    • In the long term, .mo support could be deprecated
    • n
  • nnnn
  • Security considerations due to essentially executing remotely fetched PHP filesn
    • Not really different from downloading plugins/themes from WordPress.org
    • nnnn
    • WordPress considers translations to be trusted
    • nnnn
    • Hosting providers could be blocking PHP execution in wp-content/languages
    • nnnn
    • Could potentially use checksum verifications or static analysis at install time to detect anomalies
    • n
  • n

Effort and timeline

nnnn

The 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 itu2019s important to loop in the WordPress security team and hosting providers early on.

nnnn

More time is required to test other file formats and compare results.

nnnn

Solution B: Native gettext extension

nnnn

Use the native gettext PHP extension written in C when available, instead of the custom built-in parser in WordPress.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

As of July 2023, around 66% of all localized WordPress sites have the gettext extension installed, according to information from the WordPress update requests.

nnnn

Benefits

nnnn
  • Significant performance improvements for eligible sitesn
    • Initial benchmarks show that loading time and memory usage basically do not differ from non-localized sites
    • n
  • n

Caveats and risks

nnnn
  • The gettext extension is not commonly availablen
    • Smaller incentive to implement and lower impact overall
    • n
  • nnnn
  • Requires locales to be installed on the servern
    • Servers rarely have many installed localesn
      • Locales often need to be compiled first and take up a lot of space
      • nnnn
      • WordPress on the other hand supports over 200 locales
      • n
    • nnnn
    • Potential clashes with the custom locales WordPress supportsn
      • For example, locales like pt_PT_ao90, de_DE_formal or roh might not even be supported
      • n
    • nnnn
    • Outreach to hosting providers would be necessary
    • n
  • nnnn
  • Adds maintenance overhead by essentially adding a second gettext implementation
  • nnnn
  • Poor APIn
    • Requires setting environment variables (such as LC_MESSAGES and LANGUAGE), which might not be possible or cause conflicts on certain servers/sites
    • n
  • nnnn
  • Requires symlinks or hard file copiesn
    • Symlinks might not be possible on the server; copying all translation files means doubling disk usage
    • n
  • nnnn
  • Translation files are cached by PHP, thus any translation change requires restarting the web servern
    • There are workarounds such as cache busting using random file names or fstat, however they might not work on all environments
    • n
  • nnnn
  • Has not been tested on a wider scale, despite being discussed for years
  • n

Check out the code of WP Performance Pack and Native Gettext to get a better idea of the extensionu2019s poor API.

nnnn

Effort and timeline

nnnn

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.

nnnn

Solution C: Cache translations

nnnn

Cache translations somehow to avoid expensive .mo parsing.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

Benefits

nnnn
  • Caching translations after one time .mo parsing potentially improves performance for future requests
  • n

Caveats and risks

nnnn
  • Caching using persistent object cache (e.g. Memcached, Redis) or APCu:n
    • Not available on most sites, making this not an ideal solutionn
      • Availability according to data from WordPress update requests:n
        • Memcached: ~25%
        • nnnn
        • Redis: ~25%
        • nnnn
        • APCu: ~6%
        • n
      • n
    • nnnn
    • Can potentially significantly increase cache size or exceed cache key limits
    • n
  • nnnn
  • Database caching:n
    • Moves the problem from disk reads to database reads
    • nnnn
    • Can potentially significantly increase database size
    • nnnn
    • Alternatively, use sqlite as a cache backendn
      • Untested approach
      • nnnn
      • Available on around 90% of sites
      • n
    • n
  • nnnn
  • Disk caching:n
    • Not always possible, depending on server environment
    • nnnn
    • Still causes file reads, only with fewer or other files
    • n
  • nnnn
  • Multiple cache groups (e.g. per-request or frontend/admin split)n
    • Smarter cache logic to only load translations that are needed for the majority of requests
    • nnnn
    • Can potentially significantly increase cache size
    • nnnn
    • Unlikely that different requests use very different translations
    • n
  • nnnn
  • Cache retrieval adds overheadn
    • Exact performance gains depend on implementation method and need to be measured first
    • nnnn
    • No performance gains with cold cache
    • nnnn
    • Cache invalidation logic TBD
    • n
  • n

Effort and timeline

nnnn

Given 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.

nnnn

However, the right caching strategy probably does not exist because of all the different hosting environments. Since itu2019s unrealistic for core to support multiple types of caching, this solution seems better suited for plugins rather than core.

nnnn

Solution D: Lazily evaluated translation calls

nnnn

Use lazily evaluated translation calls to reduce the number of function calls in certain cases, leading to improved performance.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

It can be integrated essentially in two ways, both of which are explained on the core ticket:

nnnn
  1. Change all translation calls to be lazily evaluated by default
  2. nnnn
  3. Make this opt-in, either with new function arguments or new functions altogether
  4. n

Benefits

nnnn
  • Reduces the number of translation lookups, in some scenarios drasticallyn
    • On a regular home page request there are ~60% less translation calls, saving around ~10ms (as measured by XHProf)
    • n
  • nnnn
  • As a side effect, solves UX issues such as #38643
  • n

Caveats and risks

nnnn
  • Depending on implementation this either breaks backward compatibility or risks not gaining enough adoptionn
    • Documentation, tooling, and developer education can help mitigate this to a certain extent
    • nnnn
    • Adoption could be done gradually, e.g. starting with an opt-in approach and eventually making it the default
    • n
  • nnnn
  • Likely will not have a significant impact on typical frontend page loads, as itu2019s mostly useful for areas like the REST API schema output, where a lot of translation calls are made without actually using the translationsn
    • Needs analysis in more scenarios to measure impact
    • nnnn
    • The REST API schema already has a workaround by using a cache in a static variable
    • n
  • nnnn
  • Does not improve situation for actually loading translation files
  • nnnn
  • Initial testing shows that this actually hurts performance due to the additional thousands of proxy objects being created
  • n

Effort and timeline

nnnn

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.

nnnn

Solution E: Optimize/Rewrite existing MO parser

nnnn

Refactor the existing MO parser in WordPress to be more performant.

nnnn

Design considerations

nnnn

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.

nnnn

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.

nnnn

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.

nnnn

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.

nnnn

Benefits

nnnn
  • Can be used without necessarily introducing another file format
  • nnnn
  • Opens up potential performance enhancements in other areas, i.e. locale switching
  • nnnn
  • Mostly maintains backward compatibility
  • n

Caveats and risks

nnnn
  • Still a risk of breaking backward compatibility
  • n

Effort and timeline

nnnn

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.

nnnn

Solution F: Splitting up translation files

nnnn

Split translation files from plugins and themes into smaller chunks to make loading them more efficient.

nnnn

Design considerations

nnnn

Depending on the projectu2019s size, translation files can be quite big. Thatu2019s why WordPress itself uses separate translation files for the admin and everything else, so that not too many strings are unnecessarily loaded.

nnnn

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)

nnnn

Benefits

nnnn
  • Faster loading times due to loading smaller files
  • n

Caveats and risks

nnnn
  • Risk of breaking backward compatibility
  • nnnn
  • Opt-in approach requires tooling and distribution changes and risks slow adoption
  • n

Effort and timeline

nnnn

Further research is required to evaluate this properly.

nnnn

Comparison

nnnn

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.

nnnn

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, itu2019s a progressive enhancement that cannot be ignored. Especially since it could pretty much eliminate the need for additional caching as in solution C.

nnnn

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.

nnnn

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.

nnnn

Existing plugins like Ginger MO and WP Performance Pack show that the existing MO parser in WordPress can be further improved (solution E).

nnnn

Benchmarks

nnnn

Now to the most interesting part: the hard numbers!

nnnn

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.

nnnn

You can find the exact setup in this wp-i18n-benchmarks GitHub repository.

nnnn

Block Theme

nnnn
LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.60 MB133.58 ms
de_DEDefault29.14 MB181.95 ms
de_DEGinger MO (MO)19.24 MB159.18 ms
de_DEGinger MO (PHP)16.98 MB138.14 ms
de_DEGinger MO (JSON)19.24 MB153.39 ms
de_DENative Gettext15.99 MB142.12 ms
de_DEDynaMo19.62 MB157.93 ms
de_DECache in APCu50.37 MB181.51 ms
en_USDefaultu270515.67 MB121.53 ms
de_DEDefaultu270529.01 MB167.67 ms
de_DEGinger MO (MO)u270519.11 MB147.19 ms
de_DEGinger MO (PHP)u270516.85 MB127.97 ms
de_DEGinger MO (JSON)u270519.11 MB144.43 ms
de_DENative Gettextu270515.86 MB129.19 ms
de_DEDynaMou270518.57 MB133.46 ms
de_DECache in APCuu270550.30 MB170.19 ms
de_DECache in object cacheu270529.07 MB173.19 ms
Benchmarks using the Twenty Twenty-Three block theme

Classic Theme

nnnn
LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.35 MB120.79 ms
de_DEDefault28.79 MB172.10 ms
de_DEGinger MO (MO)18.85 MB145.68 ms
de_DEGinger MO (PHP)16.56 MB124.73 ms
de_DEGinger MO (JSON)18.84 MB140.78 ms
de_DENative Gettext15.58 MB128.26 ms
de_DEDynaMo19.24 MB146.09 ms
de_DECache in APCu50.13 MB167.28 ms
en_USDefaultu270515.19 MB107.26 ms
de_DEDefaultu270528.59 MB154.30 ms
de_DEGinger MO (MO)u270518.64 MB133.21 ms
de_DEGinger MO (PHP)u270516.37 MB112.94 ms
de_DEGinger MO (JSON)u270518.64 MB128.94 ms
de_DENative Gettextu270515.38 MB115.11 ms
de_DEDynaMou270518.10 MB120.72 ms
de_DECache in APCuu270549.99 MB151.82 ms
de_DECache in object cacheu270528.65 MB156.36 ms
Benchmarks using the Twenty Twenty-One classic theme

Admin

nnnn
LocaleScenarioObject CacheMemory UsageTotal Load Time
en_USDefault15.42 MB139.83 ms
de_DEDefault31.92 MB187.76 ms
de_DEGinger MO (MO)20.07 MB164.94 ms
de_DEGinger MO (PHP)17.09 MB139.66 ms
de_DEGinger MO (JSON)20.06 MB160.87 ms
de_DENative Gettext15.95 MB143.43 ms
de_DEDynaMo20.58 MB166.79 ms
de_DECache in APCu58.13 MB190.38 ms
en_USDefaultu270515.66 MB112.69 ms
de_DEDefaultu270531.84 MB164.26 ms
de_DEGinger MO (MO)u270519.99 MB140.70 ms
de_DEGinger MO (PHP)u270517.01 MB118.52 ms
de_DEGinger MO (JSON)u270519.98 MB138.49 ms
de_DENative Gettextu270515.87 MB120.01 ms
de_DEDynaMou270519.73 MB120.26 ms
de_DECache in APCuu270558.07 MB162.41 ms
de_DECache in object cacheu270531.86 MB164.28 ms
Benchmarks visiting the WordPress admin

Conclusion

nnnn

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.

nnnn

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.

nnnn

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.

nnnn

Next steps

nnnn

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.

nnnn

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.

n

#core, #i18n, #performance

","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/","unixtime":1690207210,"unixtimeModified":1690207210,"entryHeaderMeta":"","linkPages":"","footerEntryMeta":"","tagsRaw":"core, i18n, performance","tagsArray":[{"label":"core","count":516,"link":"https://make.wordpress.org/core/tag/core/"},{"label":"i18n","count":30,"link":"https://make.wordpress.org/core/tag/i18n/"},{"label":"performance","count":219,"link":"https://make.wordpress.org/core/tag/performance/"}],"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F&locale=en_US","hasPrevPost":false,"prevPostTitle":"","prevPostURL":"","hasNextPost":false,"nextPostTitle":"","nextPostURL":"","commentsOpen":true,"is_xpost":false,"editURL":null,"postActions":"","comments":[{"type":"comment","id":"45276","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-fierevere odd alt thread-even depth-1","parentID":"0","contentRaw":"There are some 3rd party solutions to improve overall performance by caching translations, it may be worth looking and maybe taking some ideas or even the code.nhttps://github.com/pressjitsu/pomodoro","contentFiltered":"

There are some 3rd party solutions to improve overall performance by caching translations, it may be worth looking and maybe taking some ideas or even the code.
nhttps://github.com/pressjitsu/pomodoro

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45276","unixtime":1690208498,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45276&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":1,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690208498","hasChildren":false,"userLogin":"fierevere","userNicename":"fierevere"},{"type":"comment","id":"45277","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-mnelson4 even thread-odd thread-alt depth-1","parentID":"0","contentRaw":"While improving the performance of all sites is ideal, itu2019s most impactful on big sites, and those are most likely to have a sys admin capable of implementing solution B, native gettext support, right? Solution B offers the biggest improvement for those who care, and sounds like it would be relatively easy to implement in core.","contentFiltered":"

While improving the performance of all sites is ideal, itu2019s most impactful on big sites, and those are most likely to have a sys admin capable of implementing solution B, native gettext support, right? Solution B offers the biggest improvement for those who care, and sounds like it would be relatively easy to implement in core.

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45277","unixtime":1690210801,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45277&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":1,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690210801","hasChildren":false,"userLogin":"mnelson4","userNicename":"mnelson4"},{"type":"comment","id":"45278","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-swissspidy bypostauthor odd alt depth-2","parentID":"45276","contentRaw":"Yes, there are. While not necessarily mentioned by name, many i18n caching solutions (including Pomodoro) have been looked at & tested during this analysis. nnThe way Pomodoro works is actually very similar to https://github.com/swissspidy/wp-php-translation-files and https://github.com/swissspidy/ginger-mo, which is why it was not listed separately. After loading translations from an MO file, it caches them in a PHP file which is then loaded on subsequent request.nnIf you're curious, I've added Pomodoro to the test matrix here so we can compare raw numbers. (Spoiler: they're basically identical to the PHP file approach because, well, they're almost identical solutions)","contentFiltered":"

Yes, there are. While not necessarily mentioned by name, many i18n caching solutions (including Pomodoro) have been looked at & tested during this analysis.

n

The way Pomodoro works is actually very similar to https://github.com/swissspidy/wp-php-translation-files and https://github.com/swissspidy/ginger-mo, which is why it was not listed separately. After loading translations from an MO file, it caches them in a PHP file which is then loaded on subsequent request.

n

If youu2019re curious, Iu2019ve added Pomodoro to the test matrix here so we can compare raw numbers. (Spoiler: theyu2019re basically identical to the PHP file approach because, well, theyu2019re almost identical solutions)

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45278","unixtime":1690211486,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45278&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":2,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690211486","hasChildren":false,"userLogin":"swissspidy","userNicename":"swissspidy"},{"type":"comment","id":"45279","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-swissspidy bypostauthor even depth-2","parentID":"45277","contentRaw":"I'm interested to hear how you arrived at this conclusion. The comparison tables above clearly show that even small sites currently pay a significant speed penalty and that solution B is not necessarily the fastest solution nor the most commonly available one.nnBig sites with more control over their stack don't necessarily need core to do something. They can already implement this themselves. The vast majority of WordPress installations are smaller sites, however, and it's important to offer a solution for them as well.","contentFiltered":"

Iu2019m interested to hear how you arrived at this conclusion. The comparison tables above clearly show that even small sites currently pay a significant speed penalty and that solution B is not necessarily the fastest solution nor the most commonly available one.

n

Big sites with more control over their stack donu2019t necessarily need core to do something. They can already implement this themselves. The vast majority of WordPress installations are smaller sites, however, and itu2019s important to offer a solution for them as well.

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45279","unixtime":1690212163,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45279&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":2,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690212163","hasChildren":false,"userLogin":"swissspidy","userNicename":"swissspidy"},{"type":"comment","id":"45281","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-davidanderson odd alt thread-even depth-1","parentID":"0","contentRaw":"Has anyone considered splitting translation files into frontend/backend? It's frontend performance that people care about. Loading all the backend translations on the frontend is very inefficient, and potentially you could save a lot, right there. For example, in your backup, optimization and various other plugins, 100% of translations are for the backend... if there were a `$backend_only` parameter, that could cut out a load right away...","contentFiltered":"

Has anyone considered splitting translation files into frontend/backend? Itu2019s frontend performance that people care about. Loading all the backend translations on the frontend is very inefficient, and potentially you could save a lot, right there. For example, in your backup, optimization and various other plugins, 100% of translations are for the backendu2026 if there were a `$backend_only` parameter, that could cut out a load right awayu2026

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45281","unixtime":1690217562,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45281&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":1,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690217562","hasChildren":false,"userLogin":"DavidAnderson","userNicename":"davidanderson"},{"type":"comment","id":"45282","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-swissspidy bypostauthor even depth-2","parentID":"45281","contentRaw":"Check out solution F ;-) Itu2018s exactly that.","contentFiltered":"

Check out solution F ud83dude09 Itu2018s exactly that.

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45282","unixtime":1690219329,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45282&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":2,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690219329","hasChildren":false,"userLogin":"swissspidy","userNicename":"swissspidy"},{"type":"comment","id":"45283","postID":"107045","postTitleRaw":"I18N Performance Analysis","cssClasses":"comment byuser comment-author-mobilio odd alt thread-odd thread-alt depth-1","parentID":"0","contentRaw":"Because i'm also multi-lingual person but working heavy with WP here are mine opinion.nnDO NOT use .php files for storing languages. It will flush often PHP Object Cache and almost will full op-code cache.nnhttps://php.net/manual/en/opcache.installation.phpnnHere are recommended values = 128M and 4k files.nnBut 128M is OK for all of hostings where one hosting account = one site. But when there are one hosting account and few sites this memory quickly is full with files. And some of them (files) are staying out of opcode cache causing constantly converted to op-code, exec and drop.nnSecond is number of files - 4k. For single account = single site it's good. But for single account = few sites it's too small. And last one "interned_strings_buffer" where it's 8MB.nnFor me - values are 1024MB, 100000 files and 64MB as i remember.nnAdding more PHP files than .mo native reading or gettext implementation will full much more quickly PHP object cache. This will depredate much more performance.","contentFiltered":"

Because iu2019m also multi-lingual person but working heavy with WP here are mine opinion.

n

DO NOT use .php files for storing languages. It will flush often PHP Object Cache and almost will full op-code cache.

n

https://php.net/manual/en/opcache.installation.php

n

Here are recommended values = 128M and 4k files.

n

But 128M is OK for all of hostings where one hosting account = one site. But when there are one hosting account and few sites this memory quickly is full with files. And some of them (files) are staying out of opcode cache causing constantly converted to op-code, exec and drop.

n

Second is number of files u2013 4k. For single account = single site itu2019s good. But for single account = few sites itu2019s too small. And last one u201cinterned_strings_bufferu201d where itu2019s 8MB.

n

For me u2013 values are 1024MB, 100000 files and 64MB as i remember.

n

Adding more PHP files than .mo native reading or gettext implementation will full much more quickly PHP object cache. This will depredate much more performance.

n","permalink":"https://make.wordpress.org/core/2023/07/24/i18n-performance-analysis/#comment-45283","unixtime":1690230023,"loginRedirectURL":"https://login.wordpress.org/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2023%2F07%2F24%2Fi18n-performance-analysis%2F%23comment-45283&locale=en_US","approved":true,"isTrashed":false,"prevDeleted":"","editURL":null,"depth":1,"commentDropdownActions":"","commentFooterActions":"","commentTrashedActions":"","mentions":[],"mentionContext":"","commentCreated":"1690230023","hasChildren":false,"userLogin":"mobilio","userNicename":"mobilio"}],"postFormat":"standard","postMeta":{"isSticky":false},"postTerms":{"category":[{"label":"General","count":2393,"link":"https://make.wordpress.org/core/category/general/"}],"post_tag":[{"label":"core","count":516,"link":"https://make.wordpress.org/core/tag/core/"},{"label":"i18n","count":30,"link":"https://make.wordpress.org/core/tag/i18n/"},{"label":"performance","count":219,"link":"https://make.wordpress.org/core/tag/performance/"}],"post_format":[]},"pluginData":[],"isPage":false,"mentions":["flixos90","westonruter","joemcgill","spacedmonkey","adamsilverstein","nbachiyski","ocean90","akirk","rmccue","dd32"],"mentionContext":"","isTrashed":false,"userLogin":"swissspidy","userNicename":"swissspidy"}]

Leave a Reply

Your email address will not be published. Required fields are marked *