Optimizing front-end performance is no small issue to tackle. But since our latest version is a complete rewrite from the ground up, it’s the perfect time to apply lessons from previous versions, and improve performances all around.
At CakeMail, our product engineering team is currently made up of:
One person responsible for the front-end
One person coding the back-end
One person working on the api.
As the person responsible for the front end, you can imagine that I wanted to get the best performance possible from my work. However, I also really wanted to get the best return on investment versus time. That meant addressing the biggest bottlenecks.
The Issues
The two major issues that were slowing the app on the front-end side:
1. Always doing a full reload on all pages
2. Popups were loaded in ajax
Objective 1: no browser reload
Reloads meant loading all js, css, fonts & images, executing events, initializing scripts and libs, loading html, lots of browser repaints on every page.
Those actions take us, at best, 300ms but can go to 1, 2 or even 3 seconds depending on the network. It’s a resource intensive process for our servers given all those requests.
I wanted to keep the number of requests to a minimum. We considered using a js MVC for the front-end framework, but that wasn’t a commitment we were ready to make.
So we took another route.
RESOLUTION: BACK-END SOLUTION, FRONT-END ROUTER
When a user hits CakeMail V4 for the first time, he always receives the app template (top-bar and footer) with the css and js required for the full app (we’ll come back to this later). Then the front-end router kicks in. It checks where the user is, and request the content for that page.
When the page content is received and added to the page, the router executes a function containing the DOM events tied to the page. Each page has a js file containing one object having some functions needed for the page to work correctly – mostly form validation, uploads, datagrids…
This way, the front-end code for a particular page is simple to find and read. We use the backbone router because, in addition to being the one I knew best at the time, I find it’s also the simplest to work with, and we use a custom php mvc in the back-end. Obviously the back-end does all the heavy lifting and the javascript is used in a more ‘traditional’ way.
The improvements from using this technique were astronomical for us. Loading pages in the CakeMail app now takes about 100ms after the first load. That’s 3x to 30x faster than a normal full reload. It’s certainly something I would recommend looking at for any conventional back-end app: the speed gained is tremendous with minimal effort.
MINIFYING ALL THE JS INTO 1 FILE OR LOADING DEPENDENCIES ONLY REQUIRED FOR EACH PAGE?
Generally, using a dependencies loader, and only requiring dependencies for a given page, is the method most recommended by developers.
That’s not what I ended up doing.
The CakeMail app is not that big: some pages have less than 50 lines of javascript code. In the end, it’s just a bunch of objects bundled together that do absolutely nothing when the page loads.
In fact, in regards to size, my app code is smaller than the libraries we use globally. So now we load once and never ask again. Added bonus: I never have to wait for resources when loading a page in my app. It’s a bit longer on the first load but always faster for all other pages.
Objective 2: Loading popups with the current page.
When I use a popup, it has to be there instantly. It’s just awkward to wait more than 1 second for a form with 4 inputs.
Loading popups in the page content has it challenges but they can be easily overcome. First, I load them as underscore template. That way, they don’t impact our DOM rendering speed since script tags with unknown type are ignored by the browser.
Then I created a small wrapper around jquery data() api. Everything added as data to the dom element loading the popup is automatically passed to the popup.
That gives us dynamic popups, without the headache. For example, if I have to rename a form in a popup, I simply add the data-name attribute to the button to pass the current name to the popup.
Then what do you do when the popup information is updated but the user stays on the same page? Well, that’s a bit tricky. What I ended up doing is adding a flag update that I use when I want to update a popup. That flag will reload the page in the background and reapply the content to the popup that needs an update. That’s rarely required, but it happens.
It works. For us.
Obviously that’s not a perfect system and it’s not hard to find flaws in what I described above. But it’s a system that works well for us. It gives us popups instantly rendered without an ugly loading gif and 100ms page loads.
I’m happy to say that it’s “Mission accomplished” as far as I am concerned.