Friday, December 21, 2012

How to detect DOM changes in CSS

How to detect DOM changes in CSS
This post is for the developers out there and is quite heavy on technical details. While integrating with Gmail's new compose window we ran into the issue of detecting when a compose window was open or closed. If you have Streak installed and are using the new Gmail compose you'll notice that the controls get inserted pretty much instantly - this post is about how we accomplish that.

Traditionally the two classical methods for detecting DOM changes are 1) using a timer, or 2) binding on the DOM mutation events.

Both methods suck.

Using a timer sucks because you have a fundamental tradeoff between performance and responsiveness. Responsiveness is defined as the time it takes for the Streak controls to appear when a new compose window is created. A short timer interval gives high responsiveness but that consumes a non-trivial amount of cycles (of which there are already few) and a large interval isn't responsive.

DOM mutation events suck because there's a huge performance penalty on them and they're being deprecated from browsers in the near future.

Enter CSS.

David Walsh wrote a blog post on detecting DOM node insertions using CSS. The high level overview is that you define an animation (with a very low performance cost) and give it a unique name. You then apply that animation to the DOM node selector you're interested in. Attach a document level binding on animation complete and the event callback not only tells you which animation finished (so you can switch case on the ones you're interested in) but also gives you the node you're looking for!

While the blog post specifies the technique for detecting insertions, we realized that this technique can be generalized to send an event whenever the DOM hits a particular state that you're interested in. By carefully creating your CSS selectors and making judicial use of nth-child and nth-last-child you can know whenever a specific kind of node is inserted or removed. For removal there's an extra bit of javascript that's required to determine which node is removed, but it's not that big of a deal.

What we implemented specifically for the Gmail compose windows were two selectors and two animations which you can see here:

As you can see there are two animations and two style definitions, one for an odd number of compose windows and one for an even number of compose windows. Notice how the clip goes from 0->1 for odds and 1->0 for evens. This way every time the number of compose windows changes a new event fires. We can do things this way since in Gmail you create or destroy a compose window one at a time.