Skip to content

Optimize css()#216

Merged
lencioni merged 2 commits into
masterfrom
optimize-generateCSSRuleset
Apr 27, 2017
Merged

Optimize css()#216
lencioni merged 2 commits into
masterfrom
optimize-generateCSSRuleset

Conversation

@lencioni

@lencioni lencioni commented Mar 10, 2017

Copy link
Copy Markdown
Collaborator

After the 1.2.0 release, I noticed that most of my optimizations were
completely offset by the ordering bugfixes that were added. After some
profiling, I noticed that generateCSSRuleset was ripe for optimization.

My approach here is to reduce the amount of work that needs to be done
to a bare minimum. I accomplish this by being smarter about when to loop
and how many loops to run. In my benchmark, this PR reduces the runtime
of css() from 2750 ms to 900 ms, which is 3.05x faster.

Some of this performance improvement will only be seen when running on the client,
but on the server this should still result in ~2x faster improvement.

This is probably best reviewed commit-by-commit.

Before:

screen shot 2017-03-21 at 2 31 50 pm

After:

screen shot 2017-03-21 at 2 31 26 pm

@xymostech

@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch 2 times, most recently from 51cf81d to 5c6106b Compare March 10, 2017 20:50
Comment thread src/generate.js
// the second character to avoid colliding with Moz-prefixed
// styles. Let's find its original style's sort order.
originalStyle = elementNames[i][2].toLowerCase() + elementNames[i].slice(3);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like coverage is failing branch coverage for the else case here. Any thoughts on how to fix this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to change it from an else if to an else.

@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch 2 times, most recently from aa483df to 187056b Compare March 10, 2017 22:39
Comment thread src/ordered-elements.js
/* @flow */
/* global Map */

export default class OrderedElements {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch coverage on this file falls to 75%, but I can't see any missed branches in the HTML report. I'm guessing it might not be getting the coverage right with the Babel transform happening here. Thoughts?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured it out. The constructor was never called with arguments, so I removed the args.

@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch from 187056b to 678f11e Compare March 10, 2017 22:50
@lencioni lencioni changed the title Optimize generateCSSRuleset Optimize css() Mar 10, 2017
@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch from 678f11e to 13a7422 Compare March 11, 2017 16:50
@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage remained the same at 100.0% when pulling 13a7422 on optimize-generateCSSRuleset into c42ae31 on master.

@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch from 24ab852 to 2325e6d Compare March 11, 2017 17:56
@lencioni lencioni mentioned this pull request Mar 11, 2017
@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage remained the same at 100.0% when pulling 2325e6d on optimize-generateCSSRuleset into c42ae31 on master.

@lencioni lencioni requested a review from xymostech March 13, 2017 16:36
@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage decreased (-0.3%) to 99.675% when pulling 1ab7400 on optimize-generateCSSRuleset into c42ae31 on master.

@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage remained the same at 100.0% when pulling 54dba86 on optimize-generateCSSRuleset into c42ae31 on master.

@iancmyers

Copy link
Copy Markdown

Super excited for this!

@lencioni

Copy link
Copy Markdown
Collaborator Author

@xymostech Are there any changes you'd like me to make to this PR before merging?

@xymostech

Copy link
Copy Markdown
Contributor

@lencioni Sorry, I'd like to review this before merging but I've just been terribly busy. I'll review this today or over the weekend.

@xymostech

Copy link
Copy Markdown
Contributor

Hi! My computer broke over the weekend, so I didn't get to reviewing this... It's fixed now, I'll review this today.

@lencioni

Copy link
Copy Markdown
Collaborator Author

@xymostech no worries! I'm sorry to hear about your computer troubles. :( Glad to hear that you are back in action.

@xymostech xymostech left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow! This is an insane amount of optimization, thank you so much for doing all of this!

I'm requesting changes because I think there's a few major impacts that this is going to have, and one critical bug that I noticed (the deep merge thing). I have a bunch of general questions and some nits, but overall I'm super happy with the direction of this!

Comment thread src/generate.js
const merged /* : OrderedElements */ = styleTypes.reduce(
recursiveMerge,
new OrderedElements());
const merged = new OrderedElements();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer doing a recursive merge.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. When I did this, I thought it would be alright since the tests pass, but I think actually this is a gap in the tests. I've fixed this, and in the process I think I found and fixed an existing bug where nested objects are mutated. My new implementation does not feel the cleanest anymore, so it could probably use a little refactoring.

Comment thread src/generate.js
// value that was added by prefixAll. Let's try to figure out where it
// goes.
let originalStyle;
if (elementNames[i][0] === 'W') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems vaguely fragile. Are we really sure that the only things that are not going to be in the original array here are prefixed things, not like, renamed properties or something?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on what you mean by renamed properties?

If you look at line 284 you'll see if (!originalElements.hasOwnProperty(elementNames[i])) { which means that this should only be running on properties that were not there before the prefixAll() call but were there after it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't remember what I meant by renamed properties.. :P

What I meant in general is that are we sure that the only properties that were not there before the prefixAll() call are ones that start with Webkit, Moz, or ms? Could there be others?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked through the code of inline-style-prefixer and I was unable to find any others. @rofrischmann can you confirm?

Comment thread src/generate.js Outdated
// calculation above.
return sortOrder[key];
if (originalStyle && originalElements.hasOwnProperty(originalStyle)) {
const originalIndex = handledDeclarations.keyOrder.indexOf(originalStyle);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did your benchmark include any number of things that get prefixed? I'd imagine this might be slower in that case. On the other hand, are people going to be generating a whole bunch of styles that get prefixed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, my benchmark included some things that are prefixed and some things that are not prefixed. This is faster for both because it gets all of the work done in fewer loops and avoids creating new objects in favor of mutating.

Comment thread src/generate.js Outdated
.map(([key, value]) => `${kebabifyStyleName(key)}:${transformValue(key, value)};`)
.join("");
const rules = [];
for (let i = 0; i < handledDeclarations.keyOrder.length; i ++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no space before ++

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread src/generate.js
// multiple rules for the same key. Here we flatten to multiple
// pairs with the same key.
for (let j = 0; j < value.length; j++) {
rules.push(transformRule(key, value[j], transformValue));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just going to put them in the same order that inline-style-prefixer gave them to us, correct? Is that order in the order that we want it to be?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's correct. At least in the examples I was looking at, this seemed to be the order we wanted. Although I'm not sure if that is always true or if it will remain stable. It might be worth adding a check here to ensure that the original or unprefixed value is last.

Comment thread src/util.js
}

export const flattenDeep = (list /* : any[] */) /* : any[] */ =>
list.reduce((memo, x) => memo.concat(Array.isArray(x) ? flattenDeep(x) : x), []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, I forgot how horribly inefficient this function was. Thanks!

Comment thread package.json Outdated
},
"dependencies": {
"asap": "^2.0.3",
"can-use-dom": "^0.1.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how big is this dependency? Can we just replace this with a typeof document !== "undefined" check like we do in other places? (or replace those other checks with this?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency is very small. Here's the whole thing:

var canUseDOM = !!(
  typeof window !== 'undefined' &&
  window.document &&
  window.document.createElement
);

module.exports = canUseDOM;

I think it is best to prefer dependencies over code duplication, but if you feel strongly about it, I can inline it into this project.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, using that module seems fine! I'm just cautious about including something if it's trying to do something more complicated.

Comment thread src/prefix.js Outdated
// prefixed styles.
const prefixer = new Prefixer({
keepUnprefixed: true,
//userAgent: navigator.userAgent,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is commented out.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops!

Comment thread src/prefix.js Outdated

// We need keepUnprefixed so our sorting code knows how to order the
// prefixed styles.
const prefixer = new Prefixer({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from server-side rendering, I know one of the reasons we initially didn't go with this approach is because the code to determine which prefixes were needed (which we're doing a build time in staticPrefixData) took up a lot of space. Can you build aphrodite before and after this change and see how much bigger the resulting dist file is?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a fair point. I'll look into this when I have a moment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this takes the build size from 73 KiB to 134 KiB.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( That's a pretty big size bump. I'd like to get this landed because of the bug fix in it, but I'm worried about throwing this significant change onto users. Could we maybe pull this change out into a separate PR to discuss?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

Comment thread tests/prefix_test.js Outdated
const originalUserAgent = global.navigator && global.navigator.userAgent;
let prefix;

beforeEach(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3 great tests

@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch 2 times, most recently from ebf5d3a to a54fd13 Compare March 21, 2017 22:02
Comment thread src/inject.js
key, [val[key]], selectorHandlers, stringHandlers, false);
});

if (val instanceof OrderedElements) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we can change things so that this fork doesn't need to happen. If we can't it might be worth adding a utility function that takes either a plain object or OrderedElements instance and iterates over the keys/values for convenience.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine for now. Maybe add a TODO for later?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will add a TODO.

My main concern is less about this specific piece of code and more about what this means for folks who have added their own string handlers that will also need to account for this.

Comment thread src/ordered-elements.js
for (let i = 0; i < this.keyOrder.length; i++) {
callback(this.keyOrder[i], this.elements[this.keyOrder[i]]);
// (value, key) to match Map's API
callback(this.elements[this.keyOrder[i]], this.keyOrder[i]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I changed the order of arguments here to match how Map works.

Comment thread tests/generate_test.js
}], '.foo{color:blue !important;}');
});

it('does not mutate nested objects', () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this test exposes a bug that is currently in master that this commit fixes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be #226?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also #231

@xymostech xymostech left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes seem fine now! And I'd like to get them out soon so we can get that bug fixed. :) I would like to pull out the prefixer change so that we can think about that separately before doubling the size of Aphrodite. Sound good?

Thank you so much for all the work you've been doing. I'm learning a lot about performance optimization!

Comment thread src/generate.js
// value that was added by prefixAll. Let's try to figure out where it
// goes.
let originalStyle;
if (elementNames[i][0] === 'W') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't remember what I meant by renamed properties.. :P

What I meant in general is that are we sure that the only properties that were not there before the prefixAll() call are ones that start with Webkit, Moz, or ms? Could there be others?

Comment thread package.json
"caniuse-api": "^1.5.3",
"chai": "^3.3.0",
"coveralls": "^2.12.0",
"es6-shim": "^0.35.3",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting. I guess if it's available in node 4, it's probably fine for us to remove it.

Comment thread tests/inject_test.js
flushToStyleTag();

assertStylesInclude('@keyframes keyframe_1kmnkfo');
assertStylesInclude('@keyframes keyframe_tmjr6');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did this change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember exactly anymore, but I think it was because previously the nested objects were plain objects and part of my change (making things actually recursive) caused them to be OrderedElements objects, so they serialized differently. This is related to the code I commented on here: https://github.com/Khan/aphrodite/pull/216/files#r107288563

Comment thread src/ordered-elements.js
if (!this.elements.hasOwnProperty(key)) {
this.keyOrder.push(key);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems non-intuitive that .set() does a deep merge of the elements. Could we maybe pull this deep merge into a separate method, or if this isn't being used anywhere else, rename this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this makes sense to me. I'll try to look into this soon.

Comment thread src/inject.js
stringHandlers, useImportant);

injectGeneratedCSSOnce(key, generated);
if (alreadyInjected[key]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, that's fine, I was just curious. :)

Comment thread package.json Outdated
},
"dependencies": {
"asap": "^2.0.3",
"can-use-dom": "^0.1.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, using that module seems fine! I'm just cautious about including something if it's trying to do something more complicated.

Comment thread src/prefix.js Outdated

// We need keepUnprefixed so our sorting code knows how to order the
// prefixed styles.
const prefixer = new Prefixer({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( That's a pretty big size bump. I'd like to get this landed because of the bug fix in it, but I'm worried about throwing this significant change onto users. Could we maybe pull this change out into a separate PR to discuss?

Comment thread src/inject.js
key, [val[key]], selectorHandlers, stringHandlers, false);
});

if (val instanceof OrderedElements) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine for now. Maybe add a TODO for later?

@lencioni lencioni left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm going to split this up into a few separate PRs so we can focus on the specific changes a bit better.

Comment thread src/generate.js
// value that was added by prefixAll. Let's try to figure out where it
// goes.
let originalStyle;
if (elementNames[i][0] === 'W') {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked through the code of inline-style-prefixer and I was unable to find any others. @rofrischmann can you confirm?

Comment thread src/inject.js
key, [val[key]], selectorHandlers, stringHandlers, false);
});

if (val instanceof OrderedElements) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will add a TODO.

My main concern is less about this specific piece of code and more about what this means for folks who have added their own string handlers that will also need to account for this.

Comment thread src/ordered-elements.js
if (!this.elements.hasOwnProperty(key)) {
this.keyOrder.push(key);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this makes sense to me. I'll try to look into this soon.

Comment thread src/prefix.js Outdated

// We need keepUnprefixed so our sorting code knows how to order the
// prefixed styles.
const prefixer = new Prefixer({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

Comment thread tests/inject_test.js
flushToStyleTag();

assertStylesInclude('@keyframes keyframe_1kmnkfo');
assertStylesInclude('@keyframes keyframe_tmjr6');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember exactly anymore, but I think it was because previously the nested objects were plain objects and part of my change (making things actually recursive) caused them to be OrderedElements objects, so they serialized differently. This is related to the code I commented on here: https://github.com/Khan/aphrodite/pull/216/files#r107288563

@lencioni lencioni mentioned this pull request Mar 23, 2017
@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch from a54fd13 to 893386a Compare March 23, 2017 17:17
@lencioni

Copy link
Copy Markdown
Collaborator Author

Once #233 is merged, I'll rebase this again and it will be only 2 commits.

@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch from 893386a to 7831e48 Compare March 23, 2017 17:20
@SamyPesse

Copy link
Copy Markdown

Thanks @lencioni 🍻

Any chance someone from Khan can review it ? (@xymostech) This PR fixes #231.

lencioni and others added 2 commits April 20, 2017 11:40
We only use this in one place, so I decided to rewrite this for the
specific use-case to improve performance.

While I was at it, I found and fixed a bug that causes nested objects to
be mutated.

I decided to change the signature of OrderedElements's forEach callback
to match that of Map's forEach.

In my benchmark this brings css() down from 1470ms to 1350ms.
In my profiling, calling flattenDeep in here takes 1437ms out of
3418ms, and that's with already flattened arrays. By refactoring this
code, we can actually avoid a lot of the work done here and make this
faster. This change drops css() down from 3418ms to 2045ms.
@lencioni lencioni force-pushed the optimize-generateCSSRuleset branch from 7831e48 to 0700a57 Compare April 20, 2017 18:41

@lencioni lencioni left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xymostech I've rebased this but didn't make any code changes.

Comment thread src/inject.js
// utility method that can iterate over either a plain object, an
// instance of OrderedElements, or a Map, and then use that here and
// elsewhere.
if (val instanceof OrderedElements) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the bit that I am mostly worried about. But, it looks like string handlers can't be added by extensions, so maybe this is safe?

@elischutze

Copy link
Copy Markdown

this looks great I support this

@lencioni

Copy link
Copy Markdown
Collaborator Author

I have @xymostech's approval for these changes, so I'm going to merge this. But @xymostech or someone else with access will either need to publish a new version or give me access (I'm lencioni on npm).

@lencioni lencioni merged commit cd1069b into master Apr 27, 2017
@lencioni lencioni deleted the optimize-generateCSSRuleset branch April 27, 2017 15:39
@jlfwong

jlfwong commented Apr 27, 2017

Copy link
Copy Markdown
Collaborator

@lencioni Just gave you access! The version bump should happen with npm version, then the result committed and pushed

@lencioni

Copy link
Copy Markdown
Collaborator Author

Great! I should have some time this afternoon to take care of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants