Back to blog home

Graze.com’s custom Magento 2 checkout improves speed and performance

Snack company Graze has quickly established itself as a digital pioneer. Its award-winning Magento 2 website, built and implemented by Inviqa, generated £3 million in incremental sales revenue in its first 12 months. An emphasis on digital experiments and measurement has been key to this success.

It’s little surprise then that Graze jumped at the opportunity to drastically improve the performance of its Magento 2 checkout by allowing Inviqa to entirely rewrite it.

Here we explore why we customised the Magento 2 checkout, how we did it, and the business value Graze.com is already seeing.

Firstly though, let’s explore some of the challenges of working with the standard Magento 2 checkout.

Magento 2 checkout: the challenges

  • Difficult to make changes to the checkout in certain areas without massive overhead and excessive code changes
  • Slow load times for the checkout – even when optimised as much as possible using minification and bundling
  • Multiple bugs 
  • Difficult to style & overly complex (large number of templates)

Magento 2 has come a long way since it was first launched as a beta product around two years ago. Using KnockoutJS (KOJS) was a great improvement on the original Magento checkout. But while KnockoutJS does make it easier to extend the checkout, it can be quite tricky depending on the nature of what you’re changing.

If you need to completely change how the flow of the page works, for example, and would rather use proper URLs for each step – as opposed to hashbangs (e.g. https://www.yoursite.com/checkout/#shipping) – this will be challenging and, with the current setup, requires a lot of code changes.

Moving sections of the page around can be done via XML, but when you try to move the promo widget from the billing step to the shipping step, for example, it unravels quite quickly and a lot of modifications and hacks are needed to make it work.

The checkout also assumes too much about how you want to do things, and the components aren’t self-contained, with dependencies injected into them. This makes them too tightly coupled at times.

Magento 2 checkout: performance

Another big issue, and really the main reason for us approaching Graze about a custom checkout (aside from the developmental difficulties of changing all but the basic logic) was performance. 

The customised Magento 2 checkout takes less than 2 seconds to load

The customised Graze checkout takes less than 2 seconds to load

Without enabling minification and bundling, which combines all of the JS into a small number of bigger files to improve performance, the default checkout can take more than 10 seconds to load and render. This can take even longer on modified checkouts with more features – in some cases up to 15 seconds.

Enabling bundling and minification halves this time to around 5 seconds (locally cached assets), which is much better, however this is still nowhere near good enough for an enterprise ecommerce site. 

One of the reasons the bundled version is twice as fast is because the templates of each component are in the bundled files as opposed to making an ajax call each time for the template when the KOJS component is loaded, which really kills the load time.

Improving the Magento 2 checkout

If you look at how the Magento 2 checkout is built, you’ll realise that it’s basically just a single page application (SPA), built with Knockout JS and jQuery, which communicates with the BE via API calls with AJAX. 

This made it possible to achieve the functionality we needed for Graze. Although there was a lot of logic available already within the current checkout, it was possible to create our own SPA with the logic we needed and mimic the API calls being made (as well as creating a few new endpoints).

We looked through the current code to identify what would be reusable, what the API calls would be, and whether we would need to look at creating some new endpoints and controllers going forward. The latter would enable us to create some of the additional functionality that was needed in the Graze checkout, for example the new rewards system they wanted to implement.

Choosing VueJS 

We also needed to figure out what we would use to build the SPA. There are a number of possibilities which really would achieve the same result such as React, Angular, Ember, VueJS etc, or even just Vanilla JS. 

In the end, however, we chose to use VueJS, the main reason being that it had everything we needed to build the new checkout. It has a lot of tools already built-in or officially supported by the Vue team (the Vue Router, for example), and it is much more lightweight and faster than something like Angular.

Graze Magento 2 case study

Rewriting the checkout with ES6

Another advantage to rewriting the checkout was that we could use ES6, which makes development much quicker. There are fewer issues around scoping, as well as there being a cleaner, less bloated syntax, plus time-saving features such as ‘destructuring’ and ‘spread’ operators which we used extensively. 

The ES6 was transpiled back to ES5 using Babel and webpack. This meant that the checkout still worked on older browsers such as Internet Explorer 9/10.

The basic concept of the build

Performance was our key metric when rebuilding the checkout so we knew that loading templates via AJAX after page load for instance wouldn’t provide the best user experience and the best perceived performance versus actual performance.

To achieve this we used webpack, along with vue-loader, to ensure all our code and templates would be in one bundled file to maximise performance and eliminate the need for extra calls to the server to load templates. 

The main initialisation template was also a .phtml file so there was checkout ‘skeleton’ there at all times to improve the perceived performance further.
 
vue-loader enables you to have the styles, template, and JS for a Vue Component all in one file – similar to how Angular works. The add-on then reads the .vue files and parses the file to export the JS and templates into separate export vars, and namespaces the ‘CSS’ to make it unique to that component. 

This means the app can be organised more neatly via components with fewer files, although it’s still possible to separate the template files out by doing a node import call to an external html file when referencing the template key in the Vue component. It’s just not needed when using vue-loader.

Below is an example of the cartItem.vue file

<template>
    <li class="grid grid--bleed product-item margin-bottom">
        <span class="grid__col-4 product-image-container padding-md">
            <img class="grid__cell-img" :src="imageData[item.item_id].src" :alt="imageData[item.item_id].alt" />
        </span>
        <div class="grid__col-8 grid--align-self-center product-item-details">
            <div class="grid__cell">
                <strong>{{itemName}}</strong><br />
                <span v-for="itemOption in itemOptions">
                    {{ itemOption.value }}<br />
                </span>
                <span class="muted product-weight">({{itemWeight}})</span>
                <div class="grid product-price-summary">
                    <div class="grid__col-auto">
                        <strong>qty {{item.qty}}</strong>
                    </div>
                    <div class="grid__col-auto text-right"><strong>{{ itemPrice }}</strong></div>
                </div>
            </div>
        </div>
    </li>
</template>
<script>

    import priceFormat from './../../mixins/price-format';

    export default {
        name: 'cart-item',
        props: ['item', 'imageData'],
        data() {
            return {
                itemName: this.item.name,
                itemWeight: this.item.formatted_weight,
                itemPrice: priceFormat.formatCurrency.format(this.item.base_row_total),
                itemOptions: this.item.options
            }
        }
    }
</script>

State management

VueJS offers a centralised state management system called VueX. However, we felt it was a little overkill for what we needed on Graze, so we created a hybrid one which uses mutations to change properties within the shared state object, but doesn’t go as far as VueX does with actions, modules, and so on.

VueJS allows you to define data on a component that’s ‘watched’ (data-binding, in essence). If this is defined as coming from central storage object, it means any change to the property in the central store will trigger updates to other components that are ‘watching’ the same data.
 
For example, let’s say we have the grandTotal of the cart. The grandTotal property is stored within the sharedState object in the diagram below.

Illustration of state management in Graze's Magento 2 checkout

If, for example, the promoCode module changes the value of the grandTotal property within the sharedState object via a mutation, the change will automatically be reflected in any other component displaying this property. 

So updating it in a central state causes that change to be seen in multiple locations where it’s being displayed, as long as the source the of that data is coming from the sharedState.
 
This is essentially the same as ‘data-binding’ but is not constraining it to a single, isolated component, and instead makes it available globally for any component to use.
 
This means that, unlike KOJS where you had to define variables explicitly to be a ko.observable, in VueJS you simply define it in the data function of a component, with its source as a property in a centralised object. VueJS does the rest. 

With Graze, this enabled instant visibility of updates to the likes of shipping rates, addresses, and promo discounts, and we never had to worry about sharing the data, or whether it was updated across components.

The default Magento 2 checkout does something similar, of course, but the implementation is a lot more cumbersome (as an example, see quote.js). With Graze, there was also no need to change the values of those object values via a mutation, meaning that you could easily change those things to values that were incorrect. 

What’s more, we could easily log when a value was changed and what triggered the change – something that’s not currently possible in the default checkout.

Data persistence

In order to keep all the checkout data up-to-date and consistent across page refreshes, some of the data, such as the selected address or new address data, was stored in the localStorage or via a cookie if this wasn’t available (similar to private browsing mode in Safari). 

This really just followed the convention of the original checkout and we also just hooked into the main localStorage Key that Magento uses. This meant the expiry of the data was already handled using Magento's code.

Routing in Magento 2

Internal routing in the default Magento 2 checkout is currently done via hashbangs. There’s also the option to add a ‘new’ step. The method for doing this is a bit cumbersome, however, due to the amount of XML changes that are needed to move everything to the new ‘step’, since combining / separating logic from existing steps can be difficult. 
 
In our checkout app for Graze, however, we used the excellent Vue Router which allowed us to use html5 pushstate to use properly formed URLs for each step (with a hashbang fallback for IE9).

It also meant that each step had its own dedicated component, and we used named views to load each step into its own dedicated view container.

This gave us a number of advantages, but meant that hard page refreshes could be handled much more easily, so that the user was always shown the correct information on a new page load.

Graze on why they chose Magento 2 and Inviqa (formerly Session Digital)

API calls  

There are actually relatively few API calls needed to get the Magento 2 checkout to work. For a very basic version of the checkout, without promotions or rewards, and so on, only five API endpoints are necessary (note that Graze only handles logged-in users so guest cart endpoints were not needed).
 
These are used for estimating the shipping rates for the customer: one for a new address where the actual address information is sent in a POST to the endpoint; and one for existing addresses which uses the by-address-id endpoint.  

  • estimateShippingResource:

rest/${checkoutData.storeCode}/V1/carts/mine/estimate-shipping-methods

  • estimateShippingResourceViaAddressId:

rest/${checkoutData.storeCode}/V1/carts/mine/estimate-shipping-methods-by-address-id

The next call that’s needed is the shipping information resource which lets you progress between the shipping method step and the payment step. This requires you to send through both the billing and shipping address, along with the selected shipping method, so that they’re set-up ready for the payment step. The response of this call gives you the active payment methods available.

  • shippingInformationResource:

rest/${checkoutData.storeCode}/V1/carts/mine/shipping-information
 
The next two methods are used for making payments. They simply follow the standard Magento 2 conventions for payment methods and the paymentMethod name, plus the data needed for that specific method, is sent to the server. For Paypal there’s a slightly different endpoint: selected-payment-method.

  • paymentResource:

rest/${checkoutData.storeCode}/V1/carts/mine/payment-information

  • selectedPaymentResource:

rest/${checkoutData.storeCode}/V1/carts/mine/selected-payment-method

Handling payments  

When first embarking on this project we thought that payments could pose a problem, however all the payment options in Magento 2 are standalone modules. We had already carried out some custom work for Graze card payments and modified the current checkout to support this. 
 
Therefore, when creating the new checkout, we just had to rebuild the frontend aspect of those payment methods (in our case just Paypal and CC Payments) and mimic the calls to the API (as we already discussed). All the BE logic was completely reusable. 
 
When progressing between steps, the shipping-information API response shows which payment methods are active or not. As with the regular Magento 2 checkout, this meant that, if you disable a payment method, it won’t render that particular one.

Performance gains 

So, was it worth customising Graze’s checkout? Absolutely!

To illustrate this, there are two metrics we should look at: the actual performance and the perceived performance. 

The actual performance is the total time taken to complete the page load, whereas the perceived performance is the time taken before a customer can interact with the site (i.e. see the login / sign-up form).
 
With these figures it’s also important to note that the actual page is not cached behind Varnish Cache since it’s constantly changing. In terms of assets, however, these figures are done from a cold cache. The figures are even better when using cache.

Actual Performance (cold cache)

  • Old checkout: 11.5 seconds
  • New Checkout: 3.2 seconds

Actual Performance (cached)

  • Old checkout: 6 seconds (fully bundled and minified)
  • New Checkout ~2 seconds

Obviously these times also depend on the server load at the time and the user connection speed, but you can see the massive improvement that had been made versus the old checkout.
 
I mention the perceived performance because although you can’t really measure it as such, we did a few things to make the experience better.
 
If you visit the checkout you will always see a ‘framework’ for the checkout with loaders for each section which you see instantly whilst the app initialises. In the default checkout you just see a full screen loader with nothing behind it until the entire app has loaded.

If you come to the checkout URL and you’re not logged in, you instantly see the login / sign-up form as it is pure HTML and doesn’t need the app to load in order to be seen. This makes the perceived performance almost instantaneous on a new page load.

Concluding remarks

It was a big decision to rewrite the entire checkout in Magento 2, but the advantages to Graze were well worth the effort.

It’s now far easier to modify the checkout and, with fewer bugs, performance has improved dramatically, which was the number-one metric when embarking on this project. 

I’d like to thank Graze for letting us go ahead with this experimental piece of work, and the whole team who helped bring it to life! Take a peek at the new checkout over at graze.com.

Got questions about Magento ecommerce development? Talk to Inviqa, one of Europe's most acclaimed Magento partners

Related reading