Zepto 1.1 selector test is 1.8x slower in Firefox 26 than Firefox 25 #879

Closed
cpeterso opened this Issue Dec 10, 2013 · 20 comments

7 participants

@cpeterso

The following Zepto 1.0 vs 1.1 selector test is 1.8x slower in Firefox 26 than Firefox 25:

http://jsperf.com/zepto-1-0-vs-1-1-performance/7

                        Zepto11     Zepto10     jQuery
Firefox 25 (Release)    51,096      23,655      31,849 (operations/second)
Firefox 26 (Beta)       29,456!     18,049      30,025
Firefox 27 (Aurora)     28,984      20,318      35,839
Firefox 28 (Nightly)    29,345      19,636      36,649

The primary problem is that the zepto.Z function overrides dom.__proto__, which prevents many JIT optimizations. Overriding an object's __proto__ property is strongly discouraged and will be deprecated in ES6.

For more information, please see Firefox bug 947048 for Mozilla developers' discussion this particular Zepto test regression.

@mislav
Collaborator

Thanks for letting us know. People in the bugzilla thread speak as this is the fault of people using __proto__, but don't suggest viable alternatives using standard JavaScript. All we need is a fast method to create a Zepto collection:

  1. that is Array-like, e.g. is supports accessing individual elements via square brackets;
  2. that inherits methods defined on the $.fn object.
@DavidBruant

jQuery create objects that fit your criteria of a Zepto collection, obviously. The opposite would certainly lead to strong incompatibilities leading to Zepto not being a drop-in replacement. But jQuery never used __proto__ or anything non-standard. Why not using their technique? (I know the answer, don't bother ;-) )

__proto__ is not standard. It will be in ES6... de facto. I hope you know what de facto means in this context.

The more appropriate ES6 way of doing things involves subclassing arrays as in:

class Zepto extends Array{
  ...
}

Zepto.prototype will be your $.fn

@mislav
Collaborator

@DavidBruant How does jQuery do it? I've never looked.

That ES6 snippet you posted looks really nice but we need something that works now.

@mathiasbynens

How about #736 by @webreflection? It’s an easy step to avoiding __proto__ where possible.

@DavidBruant

@mathiasbynens Polyfilling Object.setPrototypeOf with __proto__ has the same downside, especially performance-wise. The heart of the problem is the Zepto.Z function setting __proto__ (directly or after a function call). Getter accesses to __proto__ aren't that big of a deal.

@mislav

@DavidBruant How does jQuery do it? I've never looked.
That ES6 snippet you posted looks really nice but we need something that works now.

jQuery's technique did work at the time Zepto was created, too bad you never cared to look :-/
No need to debate this further anyway. Zepto made a decision while there were alternatives (Zepto is supposed to be a jQuery replacement and jQuery source is available). The rest is arguing nanoseconds and nanobytes or bad faith.

@madrobby
Owner

@mathiasbynens Object.setProtoypeOf isn't supported anywhere, and when it will be there's no guarantee that it will be performant. Additionally, if a browser does implement performance optimizations for it, they should apply to __proto__ as well, as it's doing the same thing.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf#Browser_compatibility

@madrobby
Owner

@DavidBruant Instead of truisms, please suggest a method of speeding Zepto up, for example as a proof-of-concept patch.

@jslegers

@DavidBruant & @mathiasbynens :

Does the ES6 standard still allow us to define classes and objects the traditional ways ( http://www.phpied.com/3-ways-to-define-a-javascript-class/ )? Of are they going to deprecate those as well?

And in case they still allow defining classes and objects the traditional ways, what exactly does this new syntax offer that we don't already have? How is the new class syntax an improvement beyond mere readability for people used to writing code in languages like Java or PHP?

@DavidBruant

@jslegers This is just my opinion and I only speak for myself here:

Does the ES6 standard still allow us to define classes and objects the traditional ways ( http://www.phpied.com/3-ways-to-define-a-javascript-class/ )?

Yes. Everything that has been based on standards and works will keep working likely for eternity.

Of are they going to deprecate those as well?

I disagree with @cpeterso when he wrote that __proto__ will be deprecated. "deprecated" does not mean much in web technologies since breaking backward compatibility is impossible on the web. But setting __proto__ will continue to be a discouraged practice. Among other things because it's much harder to optimize for implementors and it can lead to hard-to-read code (not in Zepto's case where the use is very localized).
TC39 and people on the es-discuss mailing list spent lot of time discussing the use cases and usages of setting __proto__. This article by Kangax was of some influence. That's one of the reasons the class syntax emerged (the extends keyword at least).

And in case they still allow defining classes and objects the traditional ways, what exactly does this new syntax offer that we don't already have?

Subclassing builtins like Array (exactly Zepto's use case) or Date. I think there are subtle things related to super too, but I didn't look at this too closely yet.

How is the new class syntax an improvement beyond mere readability for people used to writing code in languages like Java or PHP?

Readability too indeed. I'm not a big Java or PHP dev, but I've played with TypeScript and appreciated the class syntax. It leaves no ambiguity as to the intention of the code. Instead of having to figure out "oh yeah, this function is a constructor and here they're building the prototype and it inherits from X", the class syntax makes it straightforward. You don't have to reverse-engineer the class pattern.
The class syntax makes "official" a very common pattern. In large code bases, that makes a difference. For people who come to the language and don't want to spend hours investigating how to do code reuse, the class syntax will save them time and do the right thing for them.

@madrobby
Owner

@jslegers Don't get me started about ES6, I think it's an attempt to please everyone, ending up with a language that doesn't know what it wants. There's a few good bits in there, but traditionally JavaScript has been a language that evolved slowly, adding a few features here and there, That's served it well, it's arguably the most-installed and one of the most-used languages today. The attempt to "revolutionize" it will, in all likelihood, fail.

@jslegers

@DavidBruant :

So what exactly do the standards say about extending builtins today? Last week, I experimented with the following syntax, in an attempt to add a cross-browser event mechanism for altering the DOM :

(function(element) {
    element.appendChild = function(old_remove) {
        return function(childNode) {
            this.dispatchEvent(new CustomEvent('eventAppendChild', {
                'detail': {
                    'node': childNode
                }
            }));
            return old_remove.call(this, childNode);
        }
    }(element.appendChild);
})(Element.prototype);

Quite to my surprise, I managed to get this technique to work in both Chrome and IE8 (using a polyfill for events) for every DOM mutation method I tried, but Firefox silently failed when I used it on 'setAttribute', 'removeAttribute'. I'm a bit puzzled on the reason for Firefox's different behavior on specificly those methods.

@madrobby :

The main problem I have with ES6 standards is that their implementation often involves the deprecation of useful non-standard features like __proto__, __noSuchMethod__, __defineGetter__ or mutation events in favor for newer, standardized syntaxes. By effectively removing such features in newer browsers, doing anything remotely advanced in a cross-browser fashion becomes no less a mess than it was in the days of NS4 and IE5.5 as it forces you to either polyfill the new syntax with the old syntaxes or add if/else statements for testing the various different syntaxes.

I'm all in favor of standards and am very much looking forward to actually being able to use something as cool as Proxies, but deprecating non-standard features for which no decent alternatives exist other than the ES6 standards seems like a very bad move to me at a time when ES6 is still in a draft stage and you still need to explicitly turn on the 'Enable Experimental JavaScript' feature in Chrome for quite a number of its ES6 features... __noSuchMethod__, where are you when I need you?!

@DavidBruant

@jslegers I feel we're getting a bit off-topic, but in short, there is a distinction between "builtins" defined in the ECMAScript spec and the ones defined in W3C specs. This has nothing to do with the specs themselves, but the implementations and teams in browser engines which were different for both technologies.
ES6 proposes a mechanism to extend ECMAScript builtins, but says nothing about DOM builtins (and shouldn't anyway). The spec trying to gap W3C "interfaces" and their reification in JavaScrip is WebIDL. For now, I don't think they're discussing extending Element. Note that WebComponents are a mechanism to extend HTMLElement.
On your specific code, I don't know, did you raise a bug on the Firefox side? If they don't know, they'll never fix it...

The main problem I have with ES6 standards

The expression "ES6 standards" doesn't have a meaning. ES6 will be one standard. ES6 does not deprecate anything. In general, specs never remove anything (there are some very rare exceptions).

deprecating non-standard features for which no decent alternatives exist other than the ES6 standards seems like a very bad move to me

If you have use cases that aren't covered by standard features, please raise your voice to es-discuss. Some people from TC39 go in developer conferences to ask for feedback and very few people send some. For sure they won't visit every random github issue to find the feedback they need from developers.
I strongly insist on bringing use cases and not features

__noSuchMethod__, where are you when I need you?!

A decent share (the only useful share some would argue) of __noSuchMethod__ can be reimplemented with proxies.
http://soft.vub.ac.be/~tvcutsem/proxies/ contains an example with the old Proxy API (but it can be adapted easily to the new API)

@mislav
Collaborator

This is a great discussion about the future of JavaScript, guys. However, it fails to address two main topic points:

  1. We need a way to avoid being caught in the performance regression of Firefox 26;
  2. It would be great if we moved off __proto__ entirely, being a nonstandard property that the Firefox team isn't likely to care about in the future.

PRs (actual code) welcome

@jslegers

@DavidBruant :

I haven't raised a bug yet. I'm not even sure if it's worthy to pursue such a hacky technique without solid cross-browser support.

My use case : creating a library that provides a very easy and intuitive syntax for two-way data binding between any two variables (including DOM properties).

For example, this is one syntax I've been considering :

var usersDATA = [
    {'name' : 'John', 'city' : 'Leuven' },
    {'name' : 'David', 'city' : 'Bordeaux' }
];
var usersHTML = document.getElementById('table-users');
binder.set({
    'key' : 'userhtml-to-data',
    'source' : usersHTML,
    'target' : usersDATA,
    'filter' : function(source, target) {
        // object that transforms the 'source' format to the 'target' format
    }
}, {
    'key' : 'userdata-to-html',
    'source' : usersDATA,
    'target' : usersHTML,
    'filter' : function(source, target) {
        // object that transforms the 'source' format to the 'target' format
    }
});

The problem I'm experiencing, is finding a way to capture changes to the 'source' that doesn't involve polling and that is both performance and user friendly. An observer pattern using custom JS events seemed a natural choice, but it's been giving me a lot of headaches trying to actually implement it :

  • One strategy I was considering, was encapsulating the properties of usersDATA and usersHTML directly by means of Object.defineProperty(obj, prop, options) with getter + setter, triggering a custom event whenever a property of either object is altered by means of JS. That didn't work because you can't give 'prop' the same name as the property it's setting/getting.
  • Another strategy I was considering, was leaving usersDATA and usersHTML alone and adding a 'binder.source' and 'binder.target' object that use Object.defineProperty(obj, prop, options)with getter + setter in reference to the original usersDATA and usersHTML objects. There problem here, is that you can can not capture any changes done directly to the usersDATA and usersHTML objects. It also complicates matters when adding new properties to the usersDATA objects.
  • Something similar to PHP's __set, __get and __call would have made everything a lot easier, forwarding every unknown property of 'binder.source' and 'binder.target' to the object they reference. Unfortunately, JS only had a non-standard __noSuchMethod__ and that one was removed.
  • I looked into Proxies, but the lack of support in IE and it's lack of support in Chrome without turning on its flag for experimental JS features makes them pretty useless today and in the near future as polyfilling proxies seems impossible to me.
  • I looked into Object.observe, but I had the same problem I had with Proxies. I found a polyfill ( https://github.com/jdarling/Object.observe ), but that uses polling.
  • I looked into mutation events, but they've become deprecated. I can't remember finding a polyfill for it.
  • I looked into mutation observers, but they're not supported in IE<11. Polyfills exist, but I haven't tried any yet or looked into their code.
  • I tried hacking into DOM properties and managed to succesfully hack a couple of methods using the technique described before. However, I feel very dirty for even considering this option and have become turned off by FireFox's lack of support for some of those methods.
  • I looked into Object.prototype.watch(), but that's only supported by Firefox. There does happen to be a polyfill ( https://gist.github.com/eligrey/384583 ), but it behaves differently from Firefox's native implementation. And while it does work on DOM objects, changes to eg. childNodes or innerHTML are ignored if you don't change them directly and use a method like appendChild instead.
  • I looked into Watch.js ( https://github.com/melanke/Watch.JS/blob/master/src/watch.js ), but that library uses polling by means of setInterval(loop, 50).

And yes, I agree with you and @mislav that this probably isn't the best place to discuss this. I'm also not sure es-discuss is a better place. Feel free to mention any more suitable place if anyone's interested in continuing it. I'd love to know about any technique I may be overlooking to achieve what I'm trying to achieve...

@bzbarsky

mislav, is the requirement that setting indexed properties magicall update the length, or simply that reading indexed properties work?

In the latter case, just creating an object whose proto is $.fn would work fine; you can use square bracket indexed properties on any JS object. The main magic with arrays is the auto-update of length and having Array.prototype on the proto chain (which you presumably still do if it's on the proto chain of $.fn).

If you're in the former case, I'm still looking into what your options are.

@mislav
Collaborator

@bzbarsky No, I don't think you should be able to mutate the Zepto collection. We use some array methods on Zepto collections internally but I don't consider that to be public interface. Your idea is good, since length doesn't need to be magically updating.

@madrobby
Owner

The main advantage of __proto__ is that there's no copying of objects required. We should be sure to benchmark large Zepto collections (hundreds or thousands of items).

@bzbarsky

The point is to creat the object with the correct prototype up front instead of creating it and then mutating its prototype chain. In either case you end up with the same prototype chain in the end.

@DavidBruant

Something along the lines of (just describing the structure):

var $ = {fn: Object.create(Array.prototype)}
function Zepto(){}
Zepto.prototype = Object.create($.fn);

var zeptoCollection = new Zepto()

Advice from @jdalton :

Zepto goes nodelist->array via slice->__proto__ to zepto. It could go nodelist->zepto via push

@mislav
Collaborator

__proto__ is no longer used since 24ebb7a.

@mislav mislav closed this Nov 29, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment