How We Increased Our Plugin’s Performance by 200%

Last February 2, 2017, Ann Taylor wrote the article 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right in CodeInWP. We’ve been continuously improving Page Builder Sandwich for more than a year now: we do our research, listen to our customers, so we thought Page Builder Sandwich was gonna make it to CodeInWP’s “Who Got it Right” article. We didn’t, though, because of performance. ?

So we asked Ann about it, and she replied:

…I played around with a free version a bit. It shows a lot of promise: lots of modules, pretty intuitive UI, great WYSIWYG experience, everything is dragged around easily. However, when I had around 10 modules on the page, it became hard to continue working with it because of low editing speed…

So according to her, Page Builder Sandwich has performance issues. And since we always listen to feedback, we checked it out.

Testing the Performance

We needed a way to check out PBS’s performance; since PBS mostly runs on the browser via Javascript, Chrome’s Timeline profiler was the right tool for the job. The timeline profiler can identify and measure which processes are taking place in a webpage during runtime, which is perfect for finding speed bottlenecks.

We figured we should test out the most simple of steps first – running our mouse across multiple elements and rows. Simple right? We were confident that we wouldn’t run into performance issues with something as simple as this. Unfortunately, we were wrong.

Once we hit the record button, we ran the mouse for around 2 seconds on the screen, then hit the stop button.  Here’s the resulting timeline from the profiler:

Lo and behold. It’s chock full of performance bottlenecks!

There’s a lot of stuff happening in the graph, but the most obvious issue is that there’s a lot of purple areas in it (which shouldn’t be the case). On top of that, these purple areas are visibly that very wide, and all seem to hit the peak of the graph. You’ll also notice that the graph is always full too, and that the purple areas almost never go down to zero. It seems like the browser is always busy doing something, and almost never resting.

But what do all of those purple areas really mean?

The profiler was kind enough to tell us that they’re called reflows. Here’s the definition of  what a reflow is from the Google Developer site:

Reflow is the name of the web browser process for re-calculating the positions and geometries of elements in the document.. reflow is a user-blocking operation in the browser.

So this means that running your mouse around in PBS triggers a ton of reflows. What’s what’s worse it that each reflow pauses the browser momentarily so that it can finish doing its calculations. More reflows mean more browser pauses, and the experience becomes even more janky. In simpler terms, lots of purple areas = bad.

So it turns out Ann was right: we do have a huge performance bottleneck, so our team immediately got down to business to fix it right away.

Fixing Our Reflow / Jank Problem

Before anything else, we needed to do some research to find out just what causes this reflow in the first place. We found out that apparently, a lot of common things in Javascript can provoke the browser to cause a reflow.

Almost any function that you can use to get the width, height or position of a DOM element can trigger a reflow. Paul Irish wrote an extensive list of all the properties and functions in Javascript that can do this. The bad news is that Page Builder Sandwich uses a lot of these properties and functions, all of which cause reflow:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • and a lot more…

Fix #1: Caching Expensive Reflow Processes

The most obvious solution is to NOT use those properties and functions. However, PBS relies heavily on them and we cannot make do without them.

The solution we’re left with is to trim down as much calls to those functions as possible. The hard part here was that we used those properties and functions a lot of times in different parts of PBS, and most of the time, we used them on the same DOM element multiple times. To fix this, we created our own caching mechanism for each problematic property/function.

For example, the getBoundingClientRect  function gives us the current top, left, width, height, right, and bottom pixel measurements of a DOM element. We use this function all the time to calculate the location where to draw the element outlines in PBS. Here’s the caching function we created for this:

// Cache: Our client rects are kept here, we can use this to clear them later.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Check if we already got the client rect before.
	if ( ! element._boundingClientRect ) {

		// If not, get it then store it for future use.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

Instead of calling getBoundingClientRect , we now used pbsGetBoundingClientRect . Now every time client rect gets queried, we store that value for future reference, thereby limiting the number of reflow triggers.

We also needed a way to clear the cache when the stored client rects become invalidated. The values of the stored client rect changes when the user scrolls the page or adds/changes/removes elements in the screen. So we created a cache clearing function that we can call whenever we need fresh client rect values:

var clearClientRects = function() {
	var i;
	for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
		if ( elemsWithBoundingRects[ i ] ) {
			elemsWithBoundingRects[ i ]._boundingClientRect = null;
		}
	}
	elemsWithBoundingRects = [];
};​

We also did this same caching mechanism for other expensive function calls that we usually use.

Fix #2: Grouping Together Reads & Writes Using FastDom

Implementing fix #1 increased the performance of PBS, but it wasn’t enough.

Googling a bit more, we came across an article by Wilson Page on Preventing ‘layout thrashing’. Wilson says:

Layout Thrashing (reflow) occurs when JavaScript violently writes, then reads, from the DOM, multiple times causing document reflows.

Apparently, it isn’t enough to limit your calls to specific reflow-triggering properties and functions. When you perform multiple successive “reads” and “writes” on the DOM, it also triggers a browser reflow. Here’s a typical scenario, straight from Wilson’s site:

// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers reflow)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers reflow)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame​

The example above triggers 3 reflows because the dimensions of the entire page gets invalidated after a write. The suggested fix for this is to reorder the code to bunch up all the reads and writes together so that the whole thing would only trigger 1 browser reflow:

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame​

The snippet above makes fixing the problem look easy. But in reality, In PBS, reading and writing reflow-triggering stuff happens in a lot of different places in a lot of different times during the life-cycle of PBS. Re-organizing thousands of lines of Javascript code isn’t feasible.

Luckily for us, Wilson also provided a solution in his article. He created a small utility script to help with bunching together those reads and writes, called FastDom.

Here’s some code from Page Builder Sandwich that uses FastDom and also includes our Fix #1 above:

// Bunch together the reads from other threads.
fastdom.measure( function() {

	// Use our cached client rect.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Bunch together the writes from other threads.
	fastdom.mutate( function() {
		elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
		elem.style.height = rect.height + 'px';
		elem.style.left = rect.left + 'px';
		elem.style.width = rect.width + 'px';
	}.bind( this ) );
}.bind( this ) );​

What FastDom does is that it groups together all the fastdom.measure  calls and all the fastdom.mutate  calls and executes them asynchronously, effectively grouping all the reads and writes together to lessen all the reflows in the browser.

Fix #3: Accessing Style Objects Are Costly

We also found out with the help of jsPerf that some style-getting functions were faster than the others.

In PBS, we usually need to get the current value of a specific style of an element. For example, we normally use this to get the top margin:

var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;​

But, we found out that doing this is way faster:

var marginTop = elem.style.marginTop;​

However, elem.style.marginTop  and getComputedStyle  are very different. The first one only gives us the inline styling of the element, and doesn’t take into account any stylesheets; and the second one takes into account both inline styles and stylesheets.

What’s more is that even when the getComputedStyle  result was cached, it was still really slow. As it turns out, accessing the style object itself is the slow process.

Luckily for us, PBS appends inline styles to elements, and since inline styles take precedence (most of the time) than stylesheets, there will be cases where getting the inline style value would be enough. So if we combine the two methods above, we can come up with a much faster running code:

var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
	var styles = window.getComputedStyle( elem );
	marginTop = styles.marginTop;
}​

The Results of the Code Improvements

Our 3 fixes above took a lot of research, testing and actual implementation (and delayed our Fonts feature from being released), but in the end it was all worth it.

Going back to our initial test of moving your mouse across the screen for 2 seconds. Here’s the old ugly/janky timeline:

And here’s what our new timeline looks like after the performance improvements:

As you can see, there’re less purple stuff in the graph. And there’s a noticeable difference on the amount of processes being performed by the browser. The rectangular stuff on the bottom are thinner also, which means processes are being run faster than before.

If you compare the amount of tall/wide purple areas in the old and new graphs, we’re looking at a whopping 200% performance increase! We’re also seeing performance increases across the board. Not bad at all!

Take Aways & Learnings

To be honest, we didn’t think PBS had performance issues before Ann brought it up. Mainly because we were using development-centric machines and the performance bottlenecks weren’t that apparent. Looking back, checking for the performance should have been one of the top priorities. But at least now, as of version 4.3 it’s been taken cared of. ?

For those wanting to improve their own WordPress plugin performance, especially if you’re also using heavy Javascript, here’re my take aways from this experience:

  1. Limit/cache your calls to properties/functions that trigger reflow. Use this as a guide: What forces layout/reflow. The comprehensive list.
  2. Group together “reads” and “writes” to DOM styles. Use FastDom to do this for you.
  3. Use Chrome’s Timeline profiler, and make sure you have a little reflows (purple stuff) as possible.

3 thoughts on “How We Increased Our Plugin’s Performance by 200%

  1. Wow, you guys really went an extra mile! Thanks for listening to feedback and sharing this experience. I’m happy you’ve managed to provide your users with such improvements. Keep up the good work!

Leave a Reply

Your email address will not be published. Required fields are marked *