Editing YUI attrs overhead This edit will create a new revision. Your details (optional) Name Email (won’t be displayed; might be used for Gravatar) URL Test case details Title * Published (uncheck if you want to fiddle around before making the page public) Description (in case you feel further explanation is needed)(Markdown syntax is allowed) Every time I've tried to go full-on MVC with YUI with a formal Y.Model that updates the view on model changes, I've wound up not doing it. Naïve updates on change cause reflow thrashing but even after working that out, I've canceled several prototypes after seeing models with hundreds of varying ATTRS introduce noticeable processing delays. My conclusion is that "YUI Attrs are slow" and I've shifted personal development to Ember and now Angular. At work we're tied to YUI. Main product is currently 125k sloc of first party js in yui3. I believe the product has an essential complexity around 20-25k sloc but I need to standardize on a model, implement two way databinding, and move some pieces around to get the code reduction. My desire has been to do a rewrite in another framework but I've been told that absolutely nobody else wants to try this. I know about Luke's pull request 386 for the databinding and I know Attribute perf is on the roadmap I need this sooner than later. In the interest of keeping changes to YUI minimal, I decided to monkeypatch Model to not use Attribute. A couple hours later and this is the low quality code result. With both the change events firing, it passes all Model unit tests except for the one that uses a subattr get (`model.get('foo.a')`) since the get doesn't support subattrs. The main slowdown turned out to be the Event system, as shown below. Not shown are faster options that don't match Model semantics. Skipping the object copying required to implement `.lastChange` and the event `prevVal`/`nextVal` attributes is around twice as fast as the no-event version of this code. At the top end, using get/set as pure accessor/modifiers on an object is 20x as fast as the no-event benchmark here. Put this here and wrote it up in case it's helpful. I have a significant amount of work to get to a production grade Y.Model workalike but in the meantime I think just using this and requestAnimationFrame polling the `.changed` attr will work for our perf-sensitive models. Are you a spammer? (just answer the question) Preparation code Preparation code HTML (this will be inserted in the <body> of a valid HTML5 document in standards mode) (useful when testing DOM operations or including libraries) <script src="http://yui.yahooapis.com/3.8.1/build/yui/yui-min.js"></script> <script> YUI().use('model', function(Y) { var isUndef = Y.Lang.isUndefined; window.doAttrChangeEvents = false; window.doChangeEvents = false; var Model = Y.Base.create('model', Y.Model, [], { initializer: function(o) { var idAttribute = this.__idAttribute = this.constructor.prototype.idAttribute; var sc = this.constructor.superclass; var self = this; var get = this.get; this.__attrs = Y.merge(o || {}); this.__oldAttrs = {}; this.__oldChangeName = ''; this.__new = true; this.publish('change', { preventable: false }); if (idAttribute) { if (o && o[idAttribute]) { this._set('id', this.get(idAttribute)); } else if (o && o.id) { this._set(idAttribute, o.id); } } // the isModified tests check for this, not quite sure why if (o && (o.id || o[idAttribute])) { this.__new = false; } }, getPostInit: function(name) { return this.__attrs[name]; }, _set: function(name, val) { if (name === 'initialized' && val) { // Force ATTRS initialization and lazy instantiation // since lazy attrs force a set and we're trapping that, our attrs trap // object gets initialized this.getAttrs(); this.get = this.getPostInit; this.getAttrs = this.getAttrsPostInit; this.changed = {}; this.lastChange = {}; } this.__attrs[name] = val; return this; }, save: function() { Y.Model.prototype.save.apply(this, arguments); this.changed = {}; this.__new = false; return this; }, isModified: function() { for( var k in this.changed ) { return true; } return this.__new; }, _buildChange: function(keys, prev, curr, opts){ var out = {}; for (var i = 0, ii = keys.length; i < ii; i++) { var k = keys[i]; if (prev[k] !== curr[k]) { out[k] = Y.mix({ prevVal: prev[k], newVal: curr[k], src: null }, opts, true); } } return out; }, set: function(name, val, opts) { this.changed[name] = val; this.__oldAttrs[name] = this.__attrs[name]; this.__oldChangeName = name; this.__attrs[name] = val; var updatedKeys; if (this.__idAttribute && name === 'id' || name === this.__idAttribute) { this.__attrs.id = this.__attrs[this.__idAttribute] = val; updatedKeys = ['id', this.__idAttribute]; } var changeObj = this._buildChange(updatedKeys || [name], this.__oldAttrs, this.__attrs, opts); for (var k in changeObj) { this.lastChange = changeObj if (!opts || !opts.silent) { if (doAttrChangeEvents) { this.fire(name+'Change', this.lastChange[name]); } if (doChangeEvents) { this.fire('change', Y.mix({changed: this.lastChange}, opts, true)); } } break; } return this; }, getAttrsPostInit: function() { return Y.merge(this.__attrs); }, setAttrs: function(nv, opts) { if (!nv) { return this; } if (this.__idAttribute && nv.id || nv[this.__idAttribute]) { nv.id = nv[this.__idAttribute] || nv.id; nv[this.__idAttribute] = nv.id } Y.mix(this.changed, nv, true); this.__oldAttrs = this.__attrs; this.__oldChangeName = ''; this.__attrs = Y.merge(this.__attrs, nv); var changeObj = this._buildChange(Y.Object.keys(this.__oldAttrs), this.__oldAttrs, this.__attrs, opts); var hasChanges = false; for (var k in changeObj) { hasChanges = true; if ((!opts || !opts.silent) && doAttrChangeEvents) { this.fire(name+'Change', this.lastChange[name]); } else { break; } } if (hasChanges) { this.lastChange = changeObj; if ((!opts || !opts.silent) && doChangeEvents) { this.fire('change', Y.mix({changed: this.lastChange}, opts, true)); } } return this; }, undo: function(attrNames, opts) { if (!this.__oldAttrs) { return this; } attrNames || (attrNames = Y.Object.keys(this.__oldAttrs)); if (this.__oldChangeName && !isUndef(this.__oldAttrs[this.__oldChangeName])) { attrNames = [this.__oldChangeName]; } var toUndo = {}, needUndo = false; for (var i = 0, ii = attrNames.length; i < ii; i++) { if (!isUndef(this.__oldAttrs[attrNames[i]])) { toUndo[attrNames[i]] = this.__oldAttrs[attrNames[i]]; needUndo = true; } } return needUndo? this.setAttrs(toUndo, opts) : this; }, }); window.modelAttrs = new Y.Model({foo: 0}); window.modelNoAttrs = new Model({foo: 0}); }); </script> Include JavaScript libraries as follows: <script src="//cdn.ext/library.js"></script> Define setup for all tests (variables, functions, arrays or other objects that will be used in the tests) (runs before each clocked test loop, outside of the timed code region) (e.g. define local test variables, reset global variables, clear canvas, etc.) (see FAQ) modelAttrs.setAttrs({foo: 0}); modelNoAttrs.setAttrs({foo: 0}); Define teardown for all tests (runs after each clocked test loop, outside of the timed code region) (see FAQ) Code snippets to compare Test 1 Title Async (check if this is an asynchronous test) Code modelAttrs.set('foo', modelAttrs.get('foo') + 1); Test 2 Title Async (check if this is an asynchronous test) Code doAttrChangeEvents = false; doChangeEvents = true; modelNoAttrs.set('foo', modelNoAttrs.get('foo') + 1); Test 3 Title Async (check if this is an asynchronous test) Code doAttrChangeEvents = false; doChangeEvents = false; modelNoAttrs.set('foo', modelNoAttrs.get('foo') + 1);