Skip to main content
Version: 2023.3

Performance Best-Practice Guide

When developing a high traffic web application, it is a common practice to focus on performance measures and incorporate changes to make the application even more performant. A highly optimized website drives more traffic at zero additional cost and increases the user experience a lot. To optimize the website performance, we recommend below tools and configuration. The load test performance benchmarks are noted in each section, based on ApacheBench commandline tool, for insights on comparing the average loading time of a page, before and after applying these changes.

Before we start with the testing, it is important to turn off development & debug mode by setting the following environment variables to replicate the production environment scenario:

APP_ENV=prod
APP_DEBUG=0
PIMCORE_DEV_MODE=0

PHP 8 Opcache & JIT compiler

As you know, PHP is an interpreted language, which means the interpreter parses, compiles and executes the code (opcode) everytime, when executing PHP scripts. This may result in wastage of CPU resources and execution time. This is where the OPcache extension comes in to play:

“OPcache improves PHP performance by storing precompiled script bytecode in shared memory, thereby removing the need for PHP to load and parse scripts on each request.”

PHP 8 has introduced the support for Just-In-Time Compilation (JIT), which has a great potential to additionally speed things up significantly. The JIT compiler requires Opcache to be enabled to work.

By default, JIT compiler is turned off in PHP 8. To enable it, add the following configuration to your php.ini:

opcache.enable=1 # enables the Opcache for PHP server
opcache.enable_cli=1 # enables the Opcache for CLI mode
opcache.jit_buffer_size=256M # JIT buffer memory size. 0 value disables the JIT compiler.

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en

  • Before Changes (OPcache on but JIT off)
Time taken for tests:   1.061 seconds
Time per request: 212.203 [ms] (mean)
Time per request: 10.610 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 65 182 41.2 205 225
Waiting: 64 182 41.3 205 224
Total: 66 183 41.0 205 225
  • After Changes
Time taken for tests:   1.008 seconds
Time per request: 201.617 [ms] (mean)
Time per request: 10.081 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 1
Processing: 65 175 36.9 181 241
Waiting: 65 174 36.9 180 240
Total: 66 175 36.7 181 241

Symfony Performance Best Practices

As Pimcore is based on Symfony framework for backend requests processing, it is highly recommended to comply with Symfony Performance Best Practices.

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en

  • Before Changes
Time taken for tests:   1.100 seconds
Time per request: 219.904 [ms] (mean)
Time per request: 10.995 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 2
Processing: 70 194 44.6 200 289
Waiting: 69 193 44.6 200 289
Total: 70 194 44.4 200 290
  • After Changes
Time taken for tests:   1.042 seconds
Time per request: 208.337 [ms] (mean)
Time per request: 10.417 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 1
Processing: 56 182 37.1 189 254
Waiting: 55 182 37.1 189 254
Total: 56 182 36.9 189 255

Opcache Preloading

Opcache preloading was introduced in PHP 7.4, a feature that could improve the performance of your application significantly, by preloading commonly used framework files and keeping the generated byte-code in memory.

In order to enable OPcache preloading, configure these settings in your php.ini:

opcache.preload_user=www-data # user that runs PHP(-FPM)
opcache.preload=/var/www/html/var/cache/prod/App_KernelProdContainer.preload.php # path to preload file in project's /var/cache directory

Note: memory_limit should be set to at least 200M when using preloading with Pimcore.

It is also possible to mark which classes should be preloaded or not by using Symfony service tags container.preload & container.no_preload.

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en/shop/Products/Cars/Economy-Cars/Fiat-500~p104

  • Before Changes
Time taken for tests:   0.577 seconds
Time per request: 115.394 [ms] (mean)
Time per request: 5.770 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 35 99 23.1 112 127
Waiting: 33 97 23.1 109 125
Total: 35 99 22.9 112 127
  • After Changes
Time taken for tests:   0.468 seconds
Time per request: 93.522 [ms] (mean)
Time per request: 4.676 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 32 80 17.8 79 111
Waiting: 30 78 17.7 76 110
Total: 33 81 17.6 79 111

MySQL/MariaDB Optimizations

Pimcore uses Data Access Object or DAO pattern to isolate the business logic from low level data access operations. These data access operations play a vital role in performance of Pimcore application features requiring read & write to/from the database. Therefore, it is important to tune your database configuration to be performant and to exactly fit the needs of your application.

MySQL Composite Index

If you are using Data Objects or custom database tables for data persistence, then it is beneficial to optimize these tables to have composite indices, so that query optimizer uses the composite index for queries that matches all columns in the index rather than searching the complete table. You can configure composite indices for your data objects in the class definition under General Settings.

MySQL Buffer Pool

The buffer pool is an area in main memory where InnoDB caches table and index data as it is accessed. The main purpose of buffer pool is to improve the response time of data retrieval. The server system variable innodb_buffer_pool_size can be set from 70-80% of the total available memory on a dedicated database server with only or primarily InnoDB tables.

Please follow this discussion to decide buffer pool size and update my.cnf:

[mysqld]
innodb_buffer_pool_size=5G # needs to be adjusted according to your data
  • you can also use Mysql Tuning script for more optimizations mysqltuner.pl

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en/shop/Products/Cars/Economy-Cars~c547

  • Before Changes
Time taken for tests:   2.515 seconds
Time per request: 503.024 [ms] (mean)
Time per request: 25.151 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 124 453 116.0 457 727
Waiting: 121 448 115.3 453 724
Total: 124 454 115.9 458 727
  • After Changes
Time taken for tests:   2.251 seconds
Time per request: 450.187 [ms] (mean)
Time per request: 22.509 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 123 401 83.0 413 542
Waiting: 120 397 82.5 408 539
Total: 123 401 82.8 413 542

Pimcore Caching (Redis)

Pimcore uses extensively caches for different types of data. The primary cache is a pure object cache where every element (document, asset, object) in Pimcore is cached as it is (serialized objects).

This Primary Cache is utilizing the Pimcore\Cache interface to store the objects. Pimcore\Cache utilizes a Pimcore\Cache\Core\CoreCacheHandler to apply Pimcore's caching logic on top of a PSR-6 cache implementation which needs to implement cache tagging. Pimcore uses the pimcore.cache.pool Symfony cache pool, you can configure it according to your needs, but it's crucial that the pool supports tags.

By default, Pimcore uses the Doctrine connection and write to your DB's cache_items tables. You can also utilize available cache adapters from Symfony, however, Pimcore recommends to use Redis adapter for a performance boost with optional clustering and fail-over support.

Configure Redis adapter for Pimcore\Cache using these settings:

# config/packages/cache.yaml
framework:
cache:
pools:
pimcore.cache.pool:
public: true
default_lifetime: 31536000 # 1 year
adapter: cache.adapter.redis_tag_aware
provider: 'redis://localhost'

Beware that element data gets added to the cache on first request of the data. To warm-up the cache after clearing the cache (e.g. caused by a deployment), execute bin/console pimcore:cache:warming so users experience optimal performance even on first access of an element.

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en/shop/Products/Cars~c390

  • Before Changes

Time taken for tests: 6.495 seconds
Time per request: 1299.007 [ms] (mean)
Time per request: 64.950 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 3
Processing: 386 1178 209.6 1222 1550
Waiting: 383 1169 208.5 1215 1539
Total: 386 1178 209.6 1222 1551
  • After Changes
Time taken for tests:   5.560 seconds
Time per request: 1111.956 [ms] (mean)
Time per request: 55.598 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 2
Processing: 314 1021 196.1 1010 2106
Waiting: 312 1010 192.8 998 2080
Total: 315 1021 196.0 1011 2107

Pimcore Full Page Cache

Pimcore full page cache enables you to cache the content returned by a controller action. With this cache, the same content does not need to be generated each and every time the same controller action is invoked. A request processed through the MVC framework is cached and stored in the cache pool, so on following request calls the response is served directly from the cache.

You can configure output page cache in config.yaml, as:

pimcore:
full_page_cache:
enabled: true
lifetime: 120
exclude_cookie: 'pimcore_admin_sid'
exclude_patterns: '@^/test/de@'

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en

  • Before Changes
Time taken for tests:   0.704 seconds
Time per request: 140.810 [ms] (mean)
Time per request: 7.040 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 45 120 27.7 123 166
Waiting: 45 120 27.7 122 165
Total: 46 120 27.5 123 166
  • After Changes
Time taken for tests:   0.646 seconds
Time per request: 129.298 [ms] (mean)
Time per request: 6.465 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 45 115 26.0 116 167
Waiting: 45 114 26.0 116 166
Total: 46 115 25.9 116 167

Important notice about sessions
Only use sessions when really necessary. The Pimcore full-page cache detects the usage of sessions in the code and disables itself if necessary.

Static-Page-Generator

Pimcore offers a static page generator service, which generates static HTML files out of pages (documents). These static files can be delivered directly by Apache/Nginx instead of going through complete PHP driven MVC cycle on a frontend request. This feature is not recommended for a page with dynamic content i.e. news listing, product pages, and so on, where content changes frequently.

Enable Static Page generator for a Document:

To enable automatic static page generation for a document, go to Document -> Settings -> Static Page Generator. Mark enable checkbox and define optional lifetime for static pages (which regenerates static page after lifetime) and save document.

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en/Magazine

  • Before Changes
Time taken for tests:   0.803 seconds
Time per request: 160.651 [ms] (mean)
Time per request: 8.033 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 1.6 0 15
Processing: 55 140 47.6 129 274
Waiting: 53 136 46.8 127 268
Total: 57 141 47.7 130 274
  • After Changes
Time taken for tests:   0.033 seconds
Time per request: 6.686 [ms] (mean)
Time per request: 0.334 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 1
Processing: 2 5 1.5 5 10
Waiting: 2 5 1.6 5 10
Total: 3 6 1.3 6 10

In-Template Caching

Pimcore provides a Twig extension pimcore_cache for in-template caching, which is used to cache some parts directly in the template, independent of the global caching functionality. This can be useful for templates which needs a lot of calculation or require a huge amount of objects e.g., Navigation.

You can cache part of a template like:

{% set cache = pimcore_cache("main_navigation_cache", 60) %}
{% if not cache.start() %}
{% set navStartNode = document.getProperty('navigation_root') %}

{% if not navStartNode is instanceof('\\Pimcore\\Model\\Document') %}
{% set navStartNode = pimcore_document(1) %}
{% endif %}

{% set mainNavigation = app_navigation_data_links(document, navStartNode) %}
<div class="container">
...
{{
pimcore_render_nav(mainNavigation, 'menu', 'renderMenu', {
maxDepth: 2,
ulClass: {
0: 'navbar-nav menu-links ml-4 m-auto',
1: 'dropdown dropdown-menu',
'default': 'dropdown-menu dropdown-submenu'
}
})
}}
...
</div>
{% do cache.end() %}
{% endif %}

Read more about pimcore_cache here.

Benchmarks:

On running command, ab -n 100 -c 20 http://localhost/en/Magazine

  • Before Changes
Time taken for tests:   0.808 seconds
Time per request: 161.603 [ms] (mean)
Time per request: 8.080 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 48 137 35.8 135 207
Waiting: 45 133 35.4 130 204
Total: 49 137 35.7 135 207
  • After Changes
Time taken for tests:   0.644 seconds
Time per request: 128.881 [ms] (mean)
Time per request: 6.444 [ms] (mean, across all concurrent requests)

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 41 113 28.6 114 175
Waiting: 34 109 28.3 111 169
Total: 42 113 28.4 114 175

Varnish Cache

The Varnish Cache is a so-called caching reverse proxy, that is placed in front of a server, which is responsible for delivering website content. On initial request, the varnish cache saves a copy of the server reponse in memory. In the event of subsequent requests, the response is sent directly from the memory, bypassing the server. This leads to faster response time, as no processing is required on the server side.

Note: Pimcore sends the right headers for Varnish if full-page cache is enabled

Edge Side Includes (ESI)

Edge Side Includes is a markup to manage page fragments, which can be cached and merged into one page. These fragments can have individual cache policies and can be shared on multiple pages.

Varnish supports 2 main ESI tags, which are:

esi:include
esi:remove

Symfony has a built-in support for ESI. Read Here. Read more about Varnish.

Note: ESI tags are not compatible with the Static Page Generator.