Magento 2 one-step Checkout architecture pathology

Yegor Shytikov
5 min readOct 3, 2020

Starting this post as unique research of how real Magento 2 checkout works. Magento 2 is an awesomely terrible platform however PDP catalog, Product pages just fine compared to its “innovative checkout”. It is a real and good example of how not to build software. Magento agencies charge millions of dollars for checkout customization because nobody knows how this piece of legacy code works. Show Magento 2 Checkout to any famous software architect and he will be blinded to the rest of his life.

Magento 1 has just fine Checkout functionality, however, Magento 2 invented new obfuscation methods. M2 checkout is a Single PAge application rendered by famous Magento Require/JQuery/KnockoutJS based UI components. To load this page customers need to wait several minutes to fetch all data from the terribly slow backend with session locking, and render the HTML using XML JS configuration. It was a smart move to create something terrible to avoid competition in the Magento development business, because not every good developer can spend his valuable time to develop on a crapy platform.

Ok, let’s check the Checkout index page. To add something to JS rendered SAP almost PWA application you must write you XML /view/frontend/layout/checkout_index_index.xml a file. In this file, add content similar to this:

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shipping-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shippingAddress" xsi:type="array">
<item name="children" xsi:type="array">
<!-- The name of the form the field belongs to -->
<item name="shipping-address-fieldset" xsi:type="array">
<item name="children" xsi:type="array">
<!-- the field you are customizing -->
<item name="telephone" xsi:type="array">
<item name="config" xsi:type="array">
<!-- Assigning a new template -->
<item name="elementTmpl" xsi:type="string">%Vendor_Module%/form/element/%your_template%</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>

To remove a component from layout rendering, you need to create a plugin for the \Magento\Checkout\Block\Checkout\LayoutProcessor::process method. In your plugin, implement the around method removing the corresponding layout nodes at run-time.

The following sample is an example of the around method removing a component:

unset($jsLayout['components']['checkout']['children']['steps'][%path_to_target_node%]); //%path_to_target_node% is the path to the component's node in checkout_index_index.xml
return $jsLayout;

The biggest innovation of this checkout that it is several times slower than regular PHP server-side rendered checkout.

Another big issue is the stupid Magento Knockout.JS template architecture.

Magento loads every single checkout by AJAX, not as static content and HTTP2 couldn’t load in parallel Js needs to wait for the main thread to be fetched. Stupid Magento 2 frontend UI MVC template. And it is not only Magento checkout it is also any page of Magento + Admin area.

Let's profile checkout.

To render the checkout page you neet the initial page at first.

  1. /checkout/ — 1.88 s TTFB / 293 SQL queries

And After you need to load 11 MB of JS and execute it to start send a request to receive data via AJAX Rest/GraphQL API:

2. rest/default/V1/guest-carts/2yoINgqBRn54Mgq7V0E3PmZQB4pbJQFN/payment-information -439 ms / 123SQl queries

here you will have really slow SQL queries…

SELECT
DISTINCT ea.attribute_code
FROM
salesrule_product_attribute AS a
INNER JOIN eav_attribute AS ea ON ea.attribute_id = a.attribute_id

Distinct is a performance killer. If you are having a Succesful website you will safer from this issue

The next query is executed 47 times

SELECT
eav_attribute.*
FROM
eav_attribute
WHERE
(eav_attribute.attribute_code = ?)
AND (entity_type_id = :entity_type_id)

Sales Rule

SELECT
main_table.*,
NULL AS code
FROM
salesrule AS main_table
INNER JOIN (..) AS website ON main_table.row_id = website.row_id
INNER JOIN salesrule_customer_group AS customer_group_ids ON main_table.row_id = customer_group_ids.row_id
AND customer_group_ids.customer_group_id = ?
WHERE
(
(is_active = ?)
AND (main_table.coupon_type = ?)
AND (main_table.rule_id IN ([..]))
AND (is_active = ?)
)
AND (main_table.created_in ?)
ORDER BY
sort_order ASC

3. /rest/default/V1/guest-carts/2yoINgqBRn54Mgq7V0E3PmZQB4pbJQFN/estimate-shipping-methods — 166 SQL Queries — 2.57s

And Agin :

SELECT
DISTINCT ea.attribute_code
FROM
salesrule_product_attribute AS a
INNER JOIN eav_attribute AS ea ON ea.attribute_id =

Free bonus for Magento - maybe they wanna cache it. They like cache not cacheable.

And again 47 times attributes query:

SELECT
eav_attribute.*
FROM
eav_attribute
WHERE
(eav_attribute.attribute_code = ?)
AND (entity_type_id = :entity_type_id)

Also a good candidate for the cache.

4. shipperhq_option/checkout/loadConfig/ — 2.47 s

it is a custom module.

It has again 47 the same queries:

SELECT
eav_attribute.*
FROM
eav_attribute
WHERE
(eav_attribute.attribute_code = ?)
AND (entity_type_id = :entity_type_id)

And this slow :

SELECT
DISTINCT ea.attribute_code
FROM
salesrule_product_attribute AS a
INNER JOIN eav_attribute AS ea ON ea.attribute_id = a.attribute_id

Load Product Model or Collection that it can’t befast.

This quote mas query 17 times

SELECT
quote_id_mask.*
FROM
quote_id_mask
WHERE
(quote_id_mask.quote_id = ?)

issue is here — Magento\Persistent\Observer\CheckExpirePersistentQuoteObserver

the persistent cart has a really bad realization.

5. rest/default/V1/guest-carts/2yoINgqBRn54Mgq7V0E3PmZQB4pbJQFN/totals-information 2.89 sec -150 SQL queres

Whats interestin we are having on this interesting page:

Again 47 friends of Magento checkout:

SELECT
eav_attribute.*
FROM
eav_attribute
WHERE
(eav_attribute.attribute_code = ?)
AND (entity_type_id = :entity_type_id)

really easy to cache in 1 minute and save MySQL DB from the Magento abuse.

Also Amasty module SQL without MysqL index;

SELECT
amasty_conditions_quote.*
FROM
amasty_conditions_quote
WHERE
(amasty_conditions_quote.quote_id = ?)

6. POST rest/default/V1/guest-carts/KJgBnkyijiMvz5ml0IlQPfyBXEl4Kwm1/billing-address — 6.54 s / 176 SQL queries

7 times

SELECT
main_table.section_item_id
FROM
quotation_quote_section_items AS main_table
WHERE
(item_id = ?)
ORDER BY
sort_order DESC

I believe it was a single product during checkout.

12 times EAV:

SELECT
eav_attribute.*
FROM
eav_attribute
WHERE
(eav_attribute.attribute_code = ?)
AND (entity_type_id = :entity_type_id)

7. /rest/default/V1/customers/isEmailAvailable

10. customer/section/load/?sections=messages%2Ccompany&force_new_section_timestamp=true&_=1601707905176

11. rest/default/V1/checkout/lGyLsdi5uqMlbBzxN8W2UOiY6IcjEjZP/saveInsertedInfo

12. rest/V1/directory/countries

This Query rely good example of the bad magento 2 performance:

it has just 11 SQL queries 5.37 ms total.

However, to return countries as Json Magento needs 242ms on suer fast server R5.xlarge AWS

It is Awful!

And some of these queries run several times during the checkout process. So, during checkout your need really expansive backend infrastructure to handle customers fast. By my calculation to handle single Succesful checkout on Magento you need 10 CPUs

That’s it! And it is only Backend pathology. In the next post releases, you will see frontend performance...

Magento 2 also Hess constant checkout security, spam and fraud issues…

--

--

Yegor Shytikov

True Stories about Magento 2. Melting down metal server infrastructure into cloud solutions.