The gallery in Craft: 439 SQL queries for one page
The gallery page with 82 images, each with a srcset of four transforms, had 439 SQL statements and a TTFB of 106 ms in the first version, more than WordPress with Elementor. .eagerly() loads the assets in one query, but $asset->getUrl('transform') looks up the ready transform in the imagetransformindex table with a separate query for every image and every transform. One line in the template loads the transform index for the whole gallery at once:
{%- set images = block.images.eagerly().all() -%}
{%- do craft.app.imageTransforms.eagerLoadTransforms(images, ['medium', 'large', 'thumbnail', 'mlarge']) -%}
WordPress has its own version of the N+1 problem in the same gallery: two queries per image (SELECT * FROM wp_posts WHERE ID = … and a query for the meta). Loading the gallery images in advance reduces them in the test, without changing the client's theme:
add_action( 'elementor/frontend/widget/before_render', function ( $widget ) {
if ( 'theme-gallery' === $widget->get_name() ) { // name of the theme's gallery widget
_prime_post_caches( wp_list_pluck( (array) $widget->get_settings( 'gallery' ), 'id' ), false, true );
}
} );
| Gallery of 82 images (site 3) | TTFB | SQL statements |
| WordPress + Elementor, as handed over | 66.8 ms | 260 |
| WordPress, images loaded in advance (test) | 61.7 ms | 98 |
| Craft, first version of the template | 106.0 ms | 439 |
| Craft, transforms in one query | 58.2 ms | 33 |
The same on site 1: the project list went from 67 to 34 statements, the gallery from 62 to 33. In both systems the template also "works" without this.
The same quality, different bytes
Transforms from the same WebP originals, in the same dimensions and with the same quality (80 and 82), are 8 to 11 % larger in Craft (site 1: 4.3 and 4.8 MB for all transforms, site 3: 47.8 and 51.6 MB). WordPress downscales with the Triangle filter and light sharpening, Craft (the Imagine library) with Imagick's default filter. On top of that, Craft recompresses the file by default when it is uploaded from outside the control panel: after the import all 32 originals of site 1 had different bytes than in WordPress, until we switched this off:
$asset->sanitizeOnUpload = false; // files from WordPress are already sanitised; otherwise Craft writes the WebP again
Imagick does not fit in memory_limit
ImageMagick's memory does not count towards the PHP limit, and within one process it is not released between images. Importing 308 images and the first visit to the gallery (transforms generated on display, the generateTransformsBeforePageLoad option) ended with the process being killed in the 512 MB container, with no response for curl:
exit 137
At about 1,100 transforms in one process ImageMagick reported:
cache resources exhausted ... @ error/cache.c/OpenPixelCache/3931
WordPress creates the sizes on upload, one image per request. In Craft we generate the transforms in advance with a separate command in a one-off CLI container with a higher limit:
docker run --rm -m 2g ... cs-cms-php:8.3 craft site/import/transforms # one-off CLI container, the same image
The WordPress theme without Elementor: a fatal error
When we measured "WordPress without plugins", site 1 returned a 500:
PHP Fatal error: Uncaught Error: Class "Elementor\Plugin" not found in /var/www/html/wp-content/themes/<theme>/inc/elementor.php:161
The culprit was the wp_print_footer_scripts hook that switches off Elementor's JavaScript on pages without interaction. A theme built for a page builder is tied to it more closely than the admin panel shows; for the measurement we removed this one hook with a temporary file in mu-plugins.
Re-running schema/apply in Craft 5 wiped the content
Craft 5 stores field values in the JSON column elements_sites.content under the UID of the field layout element, not of the field itself. A schema script that created new layout elements on every run "lost" all texts after the second run: they were in the database, but under the old UIDs, and resave/entries made the loss permanent. The fix: reuse the UIDs of the existing layout elements. Important for anyone who keeps a Craft schema in code.
Text from Elementor is not the text on the page
Elementor stores raw HTML, and on output WordPress runs it through wptexturize: a hyphen between words becomes an en dash (–), straight quotes become typographic ones. An import from Elementor's data would have produced different characters than on the live site, so we took the texts and titles of site 2 from the rendered HTML.
Search gives different results for the same content
WordPress searches with LIKE in post_content, where Elementor keeps an HTML copy of the page together with the alt texts and file names of the images. Craft searches an index of words from fields marked as searchable, and in Craft 5 a field is not searchable by default: the search only found titles. With the index switched on, WordPress returns 6 pages for the same word, Craft 4; in two of them the word appeared only in image file names.