When I first released Macy.js, over two years ago, I never expected it to become as popular as it has, with over 15,000 downloads on npm to date. Since the initial release, it’s had some fairly significant changes, including a full rewrite.
For those who are not familiar with Macy.js: Macy.js is a small masonry, Pinterest-style layout library. Weighing in at only 4kb when gzipped, this makes it a lightweight solution for your application.
Stop slowing down
During the first year of Macy.js’ release, I received numerous comments about Macy.js gradually becoming slower when more items were added. To fix this initially, I implemented a solution that didn’t trigger on already calculated items; this had a positive effect on speed until the page was resized. I eventually decided to rewrite Macy.js to further fix some of these issues, whilst adding some new features.
The rewrite, new features, and bug fixes
There were some feature requests which particularly stood out to me: performance, and multi-instances. Macy.js was never intended to support multiple instances per page, but feedback suggested this should be an important consideration during the rewrite.
I tried multiple methods to support multiple instances. I knew I wanted to use prototypes to make this happen, as it would help manage state between multiple instances, but using this approach alone was not suitable. With every function on the prototype, it released too many functions that were not intended to be used externally. I tried prefixing internal data with an underscore but it made everything harder to read, also having to bind
this (a side effect of event listeners) all the time became an issue. Eventually, I came up with a solution that worked, a mix of prototypes and functional programming.
I also implemented a queuing system for the recalculate events (the functions that place each item in the correct position) so it would not try to recalculate numerous times simultaneously, this helped resize events. The system has three methods: add, run, and clear. It works by adding functions to an array which are called when run is called. Run calls the first function in the array and calls itself until the array is empty or until it has been manually cleared.
I also limited my element queries to the Macy.js container which helped performance, but unfortunately introduced a bug in ie11. The issue puzzled me; I never knew what caused it, the error was never descriptive enough for me to debug:
I decided on a whim to check the support of
:scope in a query selector (which I somehow overlooked), as it turns out, this was the issue. IE11 doesn’t support the use of
:scope. Implementing a polyfill for the use of
:scope solved the issue. Although I would have prefered to leave this out, using scope came with some performance benefits when doing a recalculate.
When I first built Macy.js I wasn’t aware of the implications of adding functions to the NodeList’s prototype, until I read Todd Motto’s article on it (you can read it here). So when I noticed that was occurring, I set out to fix it in the most performant way. I researched the best way to write a forEach loop, and then I remembered seeing how Lodash’s forEach loop was quicker than a native for loop, how was this possible? Negative while loops was the answer, I ended up writing my own loop functions that implemented the negative while loop. I don’t think the performance difference is noticeable in this instance, but it allowed me to move away from overwriting NodeList’s prototype.
Events were next on my list, although it flagged as a nice to have, I thought it would be a great addition to Macy.js. I set out to work out what events I would need and how I should implement them. I implemented a super simple event system that I may plan to expand at a later date. With the first iteration of the events system, I implemented the basic
emit function. Within my internal methods, I emit events when they happen and if any ‘subscribers’ have been declared it will call the functions passing an event object which contains the current Macy.js instance, and any other data that specific event would like to pass.
In the real world
After using Macy.js in a real-world Big Bite project (read our blog post), we noticed an issue with the current image loading method, some images would be missed and the images loaded callback would never be executed. We opted to use promises to solve it. There was just one issue, IE11 doesn’t support promises. There were two options to solve this; polyfill it within Macy.js, or drop support for IE11 and require a manual polyfill if IE11 support is needed. We opted to drop support for IE11, the primary reason being that the project we were working on already used a promise polyfill, so we didn’t want to load it in twice, and allows usage of a service like https://polyfill.io to automatically provide polyfills.
Overall, Macy.js has come a long way since first its first release. Bugs have been squashed, features have been added, and it’s been through a full rewrite. Macy.js 2.1, added almost all of the features that were requested. Macy.js also received a mention in a Smashing Magazine article about masonry layouts.
To install Macy.js you can download it directly from GitHub, or install it via npm:
npm install macy
Head over to GitHub to learn how to use it.