I Want a DAMNodeInserted Event!

Have DOM Level 3 Mutation Events got you down?

There’s a long, sordid history behind DOM Level 3 Mutation Events. They’re basically the DOM Event equivalent of crack for developers: a ridiculous high of programmatic, dynamic handling of DOM manipulation, with a crash of endless, unavoidable, performance-destroying event evaluation.

John Resig detailed the plight of DOM Mutation Events in a mailing list thread, circa 2009:

Yes, DOM mutation events already exist (in Firefox and Opera – fairly reliably – and dicey in Safari). They have a huge problem, though:

They absolutely cripple DOM performance on any page which they’re enabled.

Firefox, for example, when it realizes that a mutation event has been turned on, instantly goes into an incredibly-slow code path where it has to fire events at every single DOM modification. This means that doing something like .innerHTML = “foo” where it wipes out 1000 elements would fire, at least 1000 + 1 events (1000 removal events, 1 addition event).

Mutation Events have since been deprecated, and browsers have not implemented any sort of replacement…but unknowingly, they actually have ;)

But wait! An epic hack emerges!

What I’m going to present below is a hack in the truest sense of the word, but damn, is it cool. The method I’ve devised provides the same functionality DOMNodeInserted offered, without requiring you to annihilate browser performance in the process – and it is very likely to work for a long, long time.

The Description

Basically what we’re going to do is setup a CSS keyframe sequence that targets (via your choice of CSS selector) whatever DOM elements you want to receive a DOM node insertion event for. I used a relatively benign and little used css property, clip I use outline-color in an attempt to avoid messing with intended page styles – the code once targeted the clip property, but it is no longer animatable in IE as of version 11. That said, any property that can be animated will work, choose whichever one you like.

Next I added a document-wide animationstart listener that I use as a delegate to process the node insertions. The animation event has a property called animationName on it that tells you which keyframe sequence kicked off the animation. Just make sure the animationName property is the same as the keyframe sequence name you added for node insertions and you’re good to go.

The Demo

That’s about it, pretty simple huh? Let’s see it in action – notice that the text in the divs isn’t added until their insertion into the DOM is detected:

Party time, excellent!

There you have it folks, a scope-able, performant, relatively simple method for DOM node insertion listeners in all browsers that support CSS3 Animations.

In related news: I will be accepting donations in the form of Jamba Juice gift cards, or pure gold bullion if you’re feeling especially generous.

15 comments
Steven_M
Steven_M

Thank you! This solved a problem i have with the Datatables jQuery plugin where I could not find a way to attach handlers to controls in a table cell added through mRender() (can only return html string) but to also be compatible with fnUpdate() when redraw is false.

maxcardale
maxcardale

if you substitute div.addClass('some-control'); for div.setAttribute('class', 'some-control'); then the script would be pure JS.

treshugart
treshugart

This technique is awesome, thanks Daniel! For anyone that is interested, I've adapted it into an IE9 compatible library (using Mutation Events) that uses `opacity` instead of `clip` because IE 10 and 11 don't know how to animate it.


https://github.com/treshugart/skate



ahmedkotb
ahmedkotb

i have trid it and it only works once, what could be the problem ?

Thanks 

Arthur Stolyar
Arthur Stolyar

So, in latest commit you replaced animation events detection and works with dom3 and dom4 mutations events/observers. Which browsers you want to catch with it? Also, DOMNodeInserted matches only direct children of document and other job goes to DOMContentLoaded handler. But it's not some thins new and we may did it year or two ago. So no one did it because it's small performance wins and it's easy to implement just listen DOMContentLoaded event.

David Mulder
David Mulder

 Interesting definition of a lot: 57.65% of the browsers. True, that's more than the alternative, but still not "a lot".

Randy Hudson
Randy Hudson

The real question is why do web developers want to know if a DOM node has been inserted in the first place?

Carl
Carl

Thank you very much for the reply and the additional info! > I can't envision a use-case that wouldn't be served just as well by using a more specific selector. How 'bout the example I brought up - where I'm writing a user script that will live in people's browsers and be run "on top of" every web site they visit? In this use-case, there is no way of knowing where, on a page, new content will be added after the initial construction of the page's DOM. There's no specific container element to check for, and there's no way of knowing that a text (that I want to check with my user script) is added with a specific element parent (div, span, p, font, b, i, li, h1, h2,..., etc etc). Am I to understand that, for a use-case such as this one, you would not recommend using the technique you described here? > You don't even need to do something that complex, just have it animate *to* the value you want anyway, or reset it just using CSS in your page's CSS file. Same thing here, right? That is - for my use-case, where I am not in control of the web pages this script would run on - your answer does not apply, does it? I would have to use JavaScript to get the original value of the property that is animated. Also, I guess I would have to contend with the fact that there might be other animations and event monitors set up on a page that the script is run on. Oh well, it would have been great if there were a technique that could be used even when one doesn't know which elements might be added or where they they might be placed. I just want a "heads up" if content has been added, so the script can be run again...

Carl
Carl

Wow, that's a great trick! Thanks for sharing! Immediately, my thoughts go along the line of Fábios (whose question I see you answered here http://stackoverflow.com/questions/6997826/alternative-to-domnodeinserted ). I have fiddled with a Greasemonkey script which could be greatly helped if it could detect changes to the DOM (the GM script matches certain texts on pages). Now, since the GM script is run on all web sites, there's no way of knowing the structure of the page, so I can't just monitor a particular container element. I couldn't use your method by just applying the animation to the body element, right? Because that wouldn't fire if the changes occur in a sub-element to body, right? So, then I would have to apply the animation on ALL elements (*)? Animating all elements, which could be thousands on some pages, sounds a bit much... *Hmm, is 0.001s the shortest animation time that one can use?  :-)* Also, I guess I would have to store and reset the original value of the property that is animated, so I don't risk messing with sites. Hmm, what are your thoughts on using this technique of yours on entire web pages (third party ones) - could it be done in an efficient manner?  Thanks in advance for any input you can provide!

OpenGG
OpenGG

Thank you for your detailed explanation, I have also figured out that opera 12+ added support for animation, but I changed "from {clip: rect(1px, auto, auto, auto);}to{clip: rect(0px, auto, auto, auto);}" to "from{opacity:0.9}to{opacity:1}", because it's much shorter and a bit more natural. I supposed there is a performance gap between "DOMNodeInserted" and "animationstart", since the former one has to deal with more event targets, but I never tested this theory, and I'm sorry for that.

OpenGG
OpenGG

You have done an excellent job. But it seems dont work on opera, 'Element %d has been parsed!' never shows up. And let's take a step forward. Years later, when animations become really common (thousands of nodes with animations maybe), this method wpn't be so elegant and efficient as it is now. The problem is, using javascript to apply a certain function on the later inserted nodes of a corresponding kind is never easy as css. It's always listen to a more generic event, then use a router function to filter unneeded event targets, and finnally we can get the targets. When the event is too generic, e.g. DOMNodeInserted, this listen-filter proccess will be really slow.

ahmedkotb
ahmedkotb

tried*, and i meant the event was triggered for the first time, but wasn't triggered for new events