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, I use clip
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.
That is a thing of beauty!
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.
You are not seeing the parse happen in Opera because that browser does not support CSS3 Animations (keyframes), which is the *only* requirement for this method.
I’m not sure what you mean by this: “Years later, when animations become really common (thousands of nodes
with animations maybe), this method won’t be so elegant and efficient as
it is now.”
First off, this uses animations, and animates a property that is incredibly unlikely to be used by a dev on a container element – plus you can choose whatever property you want and even reset it after so there is no lasting change. Secondly, the contention that a 0.0001s animation run on thousands of nodes will cause any more ill-effect than simply loading a page with thousands of nodes on it is incorrect, I and another dev tested this on 22,000 nodes, the result: loading 22,000 nodes is kind of slow whether you do anything to them or not – adding a benign animation hardly changes the outcome. Thirdly, if your use-case is loading an HTML file with 22,000 nodes present in its static source, you probably have some bigger to deal with first…
“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 process will be really slow.” – I assure you, a one-time property assignment on a node pre-paint is much faster than you make it out to be, and the function literally does a single truthy if() check for an event.animationName match, hardly crippling logic.
As for how feasible this is for use in the wild, it works in a *lot* of browsers in the wild right now, today: http://caniuse.com/#feat=css-animation – great if you’re building an HTML5 app targeting browsers that are not irrelevant or stagnated.
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.
There may be a slight (we’re talking 1-2ms) difference between the two, but the animationstart event is outside of the main event loop, and handles how it listens for its event triggers in a much more performant manner.
Interesting definition of a lot: 57.65% of the browsers. True, that’s more than the alternative, but still not “a lot”.
I beg to differ, it works for 59.15% of all desktop users alone, and when you factor in that mobile is the fastest growing category and it supports Android 2.0+ and iOS4+, that’s most smartphones. Pretty damn good for something you can used today.
Additionally, app developers will really like it, because they mostly target those 59% of folks (only Mozilla and Chrome have large-scale web store efforts) and the mobile case, obviously.
[…] æˆ‘è¯»åˆ°äº†è¿™ç¯‡æ–‡ç« <I Want a DAMNodeInserted Event!> , 里é¢æ到了一ç§hack, 就是用css […]
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!
To answer your questions:
“So, then I would have to apply the animation on ALL elements (*)?”
– Yes, if you wanted it to fire on all inserted or parsed elements, you would use * –> I would not recommend this, and I can’t envision a use-case that wouldn’t be served just as well by using a more specific selector.
“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? :-)*”
– The processing is not intense to animate even thousands of elements, I wouldn’t be too worried (but it depends on what you’re doing to them, the more intense the interaction the more parse time). You can actually enter a duration that is less than the 0.001s I used, but remember: this is the animation *start* event we use, so it is fired immediately
– plus we’re going from 0px to 1px, and animation is hardware accelerated in most browsers, it won’t be computationally taxing for the browser to calculate.
“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.”
– 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.
“Hmm, what are your thoughts on using this technique of yours on entire web pages”
– If you don’t go selecting all elements with * and doing a 1000x loop on each, you shouldn’t affect the page performance much at all.
Also, I created this method/hack for use in a soon-to-be-publicized web components library, you can track the progress of it here: http://csuwldcat.github.com/x-tag
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…
The real question is why do web developers want to know if a DOM node has been inserted in the first place?
This is why: http://csuwldcat.github.com/x-tag/ – http://csuwldcat.github.com/x-tag/demo.html
Why not use http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#mutation-observers ?
Because DOM Level 4 Mutation Observers are brand new and just landed in the nightly versions of WebKit and Firefox in May (2012) – in contrast, this works in IE10, Firefox 5+, Chrome 3+, Opera 12, Android 2.0+, Safari 4+, and nearly every version if iPhone Safari.
We have integrated Mutation Observers into X-Tag where they are available 😉
[…] [00:18:30] I Want a DAMNodeInserted Event […]
[…] recently posted a method for using CSS Animation Keyframes to detect node insertions via the animationstart event of a dummy […]
Actually I tried this sometime back, but did not work. It worked now, thanks.
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.
We swapped out the animation events in the X-Tag case because we needed the node insertion notification even before the styles are matched (they aren’t matched when the elements are display: none;)
[…] must go to David Walsh for his article and to Daniel Buchner for the original discovery. Also, James Allardice is working on wrapping this up into something easily deployable. Posted in: […]
[…] must go to David Walsh for his article and to Daniel Buchner for the original discovery. Also, James Allardice is working on wrapping this up into something easily deployable. Share […]
i have trid it and it only works once, what could be the problem ?
Thanks
tried*, and i meant the event was triggered for the first time, but wasn’t triggered for new events
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
if you substitute div.addClass(‘some-control’); for div.setAttribute(‘class’, ‘some-control’); then the script would be pure JS.
maxcardale yeah, I removed that, not sure how it made it in there.
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.
This post was quite helpful, especially in terms of using “outline-color” instead of “clip” for IE.
We are sensitive tho to the outline-color changing, so I instead use:
from{outline-color:initial) to {outline-color:initial}
That way, the outline-color never changes from what it currently is. And it definitely still works. Thanks!
This code will not work with <script></script>, <link>
Olexandr yes, that’s because they’re not in the body of the document, and not animatable as a result. This was specifically for non-include elements. If you should probably use Mutation Observers now that they are basically in all browsers.
Thank you so much for this beautifull hack, I use it to inline SVG sprites: https://github.com/adriengibrat/svgSprite
Thanks for the innovative technique.
However, some testings show that Google Chrome/iOS, Firefox/iOS, Safari/iOS, and Google Chrome/Android OS always fire the nodeInserted event after jQuery’s $(document).ready(function() {}) event. Other browsers seem to be fine.
I had created a demonstration on Plunker to show the issue: https://plnkr.co/edit/t6AVhcu7iTPHlKwSMHSV?p=preview
Here is a shorter url for easier mobile accesses: goo.gl/bcuoTC