Friday, August 28th, 2009

Creating a querySelector for IE that runs at “native speed”

Category: Browsers, CSS

>Hello Ajaxians, my name is Paul Young and I am the co-founder of Skybound Software. We’re the company behind Stylizer, which is a real-time CSS editing tool. We’re taking a pretty radical approach to CSS editing, and as such, a lot of what I do is “web technology research”, which is looking for better ways of doing things with a web browser, ultimately so that Stylizer can automate more of the web development process. This has allowed me the chance to discover a few things that have had significant impact on a web developer’s workflow, as was the case with State Scope Image Replacement.

In conducting some research for a future version of Stylizer I found a way to create a querySelector method for IE 7 and lower. querySelector is a very useful feature of newer web browsers. It takes a CSS selector as string parameter and returns an array containing the HTML elements that match the selector. Unfortunately it doesn’t work in IE 7 and lower… until now :)

My querySelector technique works in IE7 back to IE4 (although I only tested it back to version 6). It’s only 327 characters minified, and runs ultra-fast because it doesn’t parse strings or traverse the DOM. Keep in mind though that it’s using the CSS selector engine built into the browser, which means no funky CSS 3 selector features will work, as they weren’t implemented old versions of IE.

In order to explain how it works, I’ll need to first cover something else I discovered: Single Execution CSS Expressions (SECEs). These are an enormously powerful technique that unfortunately has been made impossible in IE8. We’re using these extensively in the built-in CSS reset feature in Stylizer to eradicate bugs and to add critical but missing features in IE 6 and 7.

About CSS Expressions

The problem with IE’s CSS expressions feature is that because they’re recalculated hundreds of times per second, they’re notoriously slow. Fortunately, I’ve found a way to ensure they’re only executed once per matched element. Examine the code below:

  1. DIV { -singlex: expression(this.singlex ? 0 : (function(t) { alert(t.tagName); t.singlex = 0; } )(this)); }

There are 3 things you need to know to understand how this works:

  • CSS expressions are executed regardless of whether the CSS property name is valid. So “-singlex” is just an arbitrary name.
  • CSS expressions are executed on every matched element. So in the case above, on every div on the page.
  • Inside a CSS expression, the “this” keyword refers to the current matched HTML element.
  • The code above checks to see if the matched element has a property called “singlex”. If it does, it just returns 0. Otherwise, it executes an inline function, passing it a reference to the matched element. Inside that function, we can perform whatever processing we want. At the end, we set a flag on the element to ensure that the function won’t be executed again.

This is how you can do complex processing inside a CSS expression without having to worry about a performance hit. The function only executes on the first execution of the expression, and then on every subsequent execution, 0 is returned instantly. The performance cost of running an if test and returning 0 is negligible, even if you’re doing it thousands of times per second. If you paste that CSS into a page with 3 div elements on it, in IE it will alert “DIV” 3 times.

The things you can do with SECE’s are basically limited only by your imagination. Here is an example of using one to fake the CSS content property in IE 7 and lower:

  1. /* Newer browsers */ DIV:after { content: "Generated content!"; } /* IE 7 and lower */ DIV { -singlex: expression(this.singlex ? 0 : (function(t) { t.innerHTML += "Generated content!"; this.singlex = 0; } )(this)); }

You can use SECEs to fake all sorts of things like generated content, min-width and min-height, CSS counters, CSS outlines, and more. But most importantly, you can…

Create a querySelector method with Single Execution CSS Expressions!

Creating a querySelector method is just a matter of dynamically adding a SECE to a page that copies a reference to each matched HTML element into a globally accessible array, and then returning that array. Examine the code below:

javascript
< view plain text >
  1. /*@cc_on if (!document.querySelector) document.querySelector = function(selector) { // Add a new style sheet to the page var head = document.documentElement.firstChild; var styleTag = document.createElement("STYLE"); head.appendChild(styleTag); // Create a globally accessible element array document.__qsResult = []; // Create the SECE that copies all matched // elements to document.__qsResult. styleTag.styleSheet.cssText = selector + "{qs: expression(this.__qs?0:(function(t){document.__qsResult.push(t);t.__qs=0;})(this));}"; // Reflow the page. Without this, the SECE won't execute. window.scrollBy(0, 0); // Clean up and return head.removeChild(styleTag); return document.__qsResult; } @*/

There you have it! The function is wrapped in a conditional compilation comment to make sure only IE 7 and lower can see it. Here is a minified version for convenience:

javascript
< view plain text >
  1. /*@cc_on if(!document.querySelector)document.querySelector=function(s){d=document;h=d.documentElement.firstChild;t=d.createElement("STYLE");h.appendChild(t);d.__q=[];t.styleSheet.cssText=s+"{x:expression(this.__q?0:(function(t){document.__q.push(t);t.__q=0;})(this));}";window.scrollBy(0, 0);h.removeChild(t);return d.__q;}@*/

I hope you can make use of SECEs and this querySelector method. Maybe it even has a place in jQuery? I don’t know. (John Resig, are you reading? :)

UPDATE

After thinking this through a little further, I realized that you don’t need to use a SECE at all for this, just a CSS expression added with JavaScript will do. Also, one of the commenters (Jordan1) pointed out that the code wouldn’t return elements that had already been queried once. I’ve posted an update to the code below that should rectify the issue:

javascript
< view plain text >
  1. /*@cc_on if (!document.querySelector)
  2.         document.querySelector = function(selector)
  3.         {
  4.             var head = document.documentElement.firstChild;
  5.             var styleTag = document.createElement("STYLE");
  6.             head.appendChild(styleTag);
  7.             document.__qsResult = [];
  8.            
  9.             styleTag.styleSheet.cssText = selector + "{x:expression(document.__qsResult.push(this))}";
  10.             window.scrollBy(0, 0);
  11.             head.removeChild(styleTag);
  12.            
  13.             var result = [];
  14.             for (var i in document.__qsResult)
  15.                 result.push(document.__qsResult[i]);
  16.             return result;
  17.         }
  18.     @*/

NOTE: Fancy writing a guest post for Ajaxian? Got some tips on content that we haven’t covered? Please email us, or tweet me :)

Posted by Dion Almaer at 6:13 am
31 Comments

++++-
4 rating from 52 votes

31 Comments »

Comments feed TrackBack URI

This is the coolest hack I’ve seen in a while. Bravo!

Comment by aheckmann — August 28, 2009

Cool, but not as original as one might think. ;-)
Dean Edwards had figured out this technique 3 years ago: http://dean.edwards.name/weblog/2006/03/faster/

Comment by LeaVerou — August 28, 2009

Wow! Gread job!
Hope John Resig will see this :-)

Comment by Tuzemec — August 28, 2009

You can force IE to execute CSS expression only once if needed. Check this great post:
One-time execution of IE CSS expressions

Comment by bkuzmic — August 28, 2009

CSS expressions were introduced in IE5, so this technique is compatible from IE5-IE7, not IE4-IE7. Same with Conditional Compilation (or at least I assume so, since I know Conditional Comments were introduced in IE5).

Yeah, I don’t think there are many people that need to support IE4, but just pointing that out.

Comment by blepore — August 28, 2009

As noted before, this topic has been explored before by Dean Edwards, et. al. back in 2006.

Personally, I’m hesitant to use it for a couple reasons:
– IE CSS Expressions are notorious for having excessive overhead.
– There’s no clear way to deal with error handling. What happens if you use a selector that isn’t supported? (Attribute selectors, child selectors, etc. etc.)
– Is this capable of working before the DOM has loaded? How about on DOM fragments? disconnected DOM nodes? XML documents? or even on non-HTML document root nodes?

If it was proved that the overhead was minimal and that error handling was possible I’d probably check this out more – but mostly just for the case where the root context is ‘document’ (since other cases really aren’t supported by this technique).

Comment by JohnResig — August 28, 2009

As I (vaguely) recall, one problem with this method and perhaps a strong reason why it was never considered viable in back when it was invented, is that any element may only have 1 expression. If you use an expression for something else, then this either damages the CSS, or doesn’t select the element.

Comment by Borgar — August 28, 2009

The “expression” isn’t executed just once, it still has to run though the JS engine every redraw. I’ve tried this technique to implement mutation events for IE. It didn’t work because of another reasons, node deletion. But, I noticed that it introduced quite a lot of mouse lag.

Also, the sub-par CSS selector in IE is often the problem we try to fix by using external selector engines.

Comment by Stakka — August 28, 2009

I guess you haven’t read my comment before – expression can be executed only once. One another example of using it, the right way:
Vertically Center Multi-Lined Text.

Comment by bkuzmic — August 28, 2009

@Borgar: No, you can have as many of these as you want on an element by setting a different “has run” flag. Note that what I’m proposing is significantly different from what Dean Edwards had described.

Comment by paulyoung — August 28, 2009

I like the method that @bkuzmic’s link suggests… which is to achieve single execution of expressions by overwriting the expression after first run. The trick that link describes is that some properties can overwrite themselves, and some can’t. So the article suggests using a “helper” attribute that CAN overwrite itself, to dynamically set another property.

“clear” can overwrite itself, so it’s a good candidate as the helper attribute. And this means it CAN be reused over and over again. So, it might look like this:

.elem {
background-position: inherit;
*clear: expression(
style.backgroundPosition = parentNode.style.backgroundPosition,
style.clear = “none”, 0
);
}

So, clear (with star hack to keep to IE6/7) is used to set the backgroundPosition property to behave as the standard “inherit” from it’s parent object (which IE doesn’t support). Notice the setting of style.clear = “none” is what makes it single-execution. Oh, and some other IE quirk… the final “, 0″ is needed to prevent hanging. crazy.

The problem with the OP method is, as pointed out already, the expression WILL still be evaluated over and over, but just short circuited with the flag. So there’s still overhead.

This property overwrite concept seems a better hack way to achieve the single-execution.

Comment by shadedecho — August 28, 2009

@paulyoung: My memory of longwinded overly detailed code discussions apparently doesn’t stretch that far back. :-)

I don’t know how practical this is for the libraries, but for a “contained” application (like yours) this looks great.

Comment by Borgar — August 28, 2009

@shadedecho: If I’m not mistaken, the slowness of traditional CSS expressions is caused by:

1) Having to reflow the layout thousands of times.
2) Having to perform the actual processing thousands of times (such as, retrieving the width of the containing element, and performing some type of processing on it)

I don’t believe a reflow of the page is caused by setting a CSS property that doesn’t even register in the browser to 0. And the expense of running the equivalent of “if (true) return 0″ is computationally zero. From my tests there doesn’t appear to be any noticeable performance penalty incurred by this technique.

Comment by paulyoung — August 28, 2009

@shadedecho: Wouldn’t it be better to just remove the expression entirely using the .removeExpression() method?

Comment by timcameronryan — August 28, 2009

Shouldn’t that be querySelectorAll? It returns a collection, not a single element.

Comment by keeto — August 28, 2009

is basically what Dean did before and what I have used a while ago for Essential selector except it works faster thanks to direct node push but I agree with John, there are too many limits and performances are still not that “fantastic”.

I would use this strategy for a getElementsByClassName rather than querySelector, in that case everything will be more “reasonable” and meaningful.

Comment by WebReflection — August 28, 2009

So to prevent it from recalculating forever, you put a “singlex” flag on the node. But what about afterwards when you want to perform another querySelector that happens to select the same node again?

Comment by Jordan1 — August 28, 2009

Clever hack!

Comment by Brad Neuberg — August 28, 2009

@timcameronryan: .removeExpression() can’t be called from within the expression itself.

Comment by Stakka — August 28, 2009

I think Jon’s comment covers most of it, but it should also be noted that this technique is ALSO limited to whatever CSS selector support IE shipped in a particular version. Oof.

That said, I love how small it is.

Regards

Comment by slightlyoff — August 28, 2009

Someone should write a jQuery plugin for simple selectors which uses this.

Comment by Darkimmortal — August 28, 2009

@Darkimmortal … a jQuery plugin explicitly requires jQuery, which includes Sizzle, so a pug-in would be something extra rather than something less, or something faster … do you agree?

Comment by WebReflection — August 28, 2009

The limitation to supported selectors might not be that big of an issue when viewed from a library’s perspective. Worst case you only use this engine when the selector is supported, and fall back to the existing selector engines for other rules.

This should at least be tested and benchmarked in detail, because while it’s an ugly hack, it it makes IE faster to work with, I’m all for it.

Comment by Joeri — August 29, 2009

Interesting, however I don’t see this as best practice. IE Expressions blur the separation of style and behaviour.

Also, as they’re javascript wrapped in css rules, they fail if the user has js turned off so you should still style the element in css alone to allow for a no js scenario.

Any reason why you wouldn’t write this in a js file instead?

Comment by adambankin — August 29, 2009

I did a couple of tests in IE8 and IE7 emulation over SlickSpeed test page.
This method does not seem to be reliable, it returns for example 6 divs rather than 46 with selector “div”.
52 if I scrollBy 2 tmes (46 + 6 found first time duplicated)
Also I do not get some part of the code, like the second push over an array to return an array. The function could simply return

document.__qsResult.splice(0, document.__qsResult.length);

without overheads. Finally, it seems to be faster via addRule and removeRule, rather than appendChild/removeChild via
var style = document.styleSheets[0] || document.createStyleSheet()

I think we need a lot of test cases but being expressions something not easy to control, behavior speaking, I am not sure how much this technique could be used. The scrollBy is for example something I do not like, but it is necessary, while I have to say this technique could be faster than the one with counters over all nodes.

Regards

Comment by WebReflection — August 30, 2009

Paul,
nice trick, really…It is good some developers put more efforts by looking to other alternatives, even if this is not well rewarded by the average readers.

I tried this technique (based on Dean Edwards findings) in several ways in my NWMatcher selector engine to be able to speed up IE browser. I was nearly crying when I realized I couldn’t use that reliably :-)

The trick could work until the number of elements remains low enough (200/300 max). It will start to slow down the browser with more.

@stakka, you are correct, but the “.removeExpression()” can be called within a setTimeout() of 0 milliseconds to achieve the removal of the rules externally from the handler itself. Still I am not sure the process is completely stopped or can be stopped in such a way. I did some test also by enabling/disabling the specific stylesheet containing the expression but not much success either. The only way I found to stop that is to completely overwrite the expression in the rule then remove it.

In IE8 mode this is completely gone, so if it works it will remain a good hack for IE7 and IE6, not applicable for me in a reliable library.

Have fun ! Best Regards.

Comment by dperini — August 31, 2009

Diego, there is a counter trick as well and the point is that we do not need to force “layout refresh”, which could be extremely slow if the DOM is loads of nodes and styles. I need to test more but since the counter trick seems to be activated as soon as you “touch” the node, maybe this strategy over a “tricky” loop will work as expected. Dunno though, would be nice if one of us find some concrete solution and will post it here. Best Regards.

Comment by WebReflection — August 31, 2009

I published a similar, but much faster approach here:
http://weblogs.asp.net/bleroy/archive/2009/08/31/queryselectorall-on-old-ie-versions-something-that-doesn-t-work.aspx
The thing is, it’s still way slower than jQuery. So bottom line is, it’s futilie to try to hack IE6/7 into exposing its native CSS selection engine. JavaScript implementations are now fast and mature enough that they will outperform anything else anytime.

Comment by BertrandLeRoy — September 1, 2009

I admire you guys!

Comment by yuchangchun — September 1, 2009

One can also use this approach:

behavior: expression(
(this.runtimeStyle.behavior="none") && (
...
) );

I am using this technique to
fix PNG transparency in IE6 (without an HTTP request).

Comment by TJK — September 3, 2009

Hi, i’m quite successfully using your hack in my website and now IE7 recognize querySelector method (before it didn’t know what is).

Unfortunately, I use to apply some style (width) to elements that I grabbed with the querySelector, so IE7 now reports me another error:
“Unable to set value of the property “width”: object is null or undefined”

the code is:
var Object = document.querySelector(‘div[class="class"]‘);
Object.style.width = ’100px’;

I checked whether Object is a nodeType text or element and I got that’s element…so I really don’t know how to solve this..

thanks in advance

Comment by asnothingelse — July 6, 2012

Leave a comment

You must be logged in to post a comment.