-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize css() #216
Optimize css() #216
Conversation
51cf81d
to
5c6106b
Compare
src/generate.js
Outdated
// 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); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
aa483df
to
187056b
Compare
src/ordered-elements.js
Outdated
@@ -1,5 +1,4 @@ | |||
/* @flow */ | |||
/* global Map */ | |||
|
|||
export default class OrderedElements { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
187056b
to
678f11e
Compare
678f11e
to
13a7422
Compare
24ab852
to
2325e6d
Compare
Super excited for this! |
@xymostech Are there any changes you'd like me to make to this PR before merging? |
@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. |
Hi! My computer broke over the weekend, so I didn't get to reviewing this... It's fixed now, I'll review this today. |
@xymostech no worries! I'm sorry to hear about your computer troubles. :( Glad to hear that you are back in action. |
There was a problem hiding this comment.
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!
const merged /* : OrderedElements */ = styleTypes.reduce( | ||
recursiveMerge, | ||
new OrderedElements()); | ||
const merged = new OrderedElements(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/generate.js
Outdated
// value that was added by prefixAll. Let's try to figure out where it | ||
// goes. | ||
let originalStyle; | ||
if (elementNames[i][0] === 'W') { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
src/generate.js
Outdated
// calculation above. | ||
return sortOrder[key]; | ||
if (originalStyle && originalElements.hasOwnProperty(originalStyle)) { | ||
const originalIndex = handledDeclarations.keyOrder.indexOf(originalStyle); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/generate.js
Outdated
.map(([key, value]) => `${kebabifyStyleName(key)}:${transformValue(key, value)};`) | ||
.join(""); | ||
const rules = []; | ||
for (let i = 0; i < handledDeclarations.keyOrder.length; i ++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: no space before ++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
src/generate.js
Outdated
// 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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@@ -21,9 +21,6 @@ export const mapObj = ( | |||
return mappedObj; | |||
} | |||
|
|||
export const flattenDeep = (list /* : any[] */) /* : any[] */ => | |||
list.reduce((memo, x) => memo.concat(Array.isArray(x) ? flattenDeep(x) : x), []); |
There was a problem hiding this comment.
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!
package.json
Outdated
"npm-run-all": "^1.7.0", | ||
"nyc": "^6.4.4", | ||
"rimraf": "^2.5.2", | ||
"webpack": "^1.12.2" | ||
}, | ||
"dependencies": { | ||
"asap": "^2.0.3", | ||
"can-use-dom": "^0.1.0", |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/prefix.js
Outdated
// prefixed styles. | ||
const prefixer = new Prefixer({ | ||
keepUnprefixed: true, | ||
//userAgent: navigator.userAgent, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops!
src/prefix.js
Outdated
|
||
// We need keepUnprefixed so our sorting code knows how to order the | ||
// prefixed styles. | ||
const prefixer = new Prefixer({ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
tests/prefix_test.js
Outdated
const originalUserAgent = global.navigator && global.navigator.userAgent; | ||
let prefix; | ||
|
||
beforeEach(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<3 great tests
ebf5d3a
to
a54fd13
Compare
src/inject.js
Outdated
key, [val[key]], selectorHandlers, stringHandlers, false); | ||
}); | ||
|
||
if (val instanceof OrderedElements) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
} | ||
|
||
forEach(callback /* : (string, any) => void */) { | ||
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]); |
There was a problem hiding this comment.
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.
@@ -134,6 +154,24 @@ ${formatStyles(actual)} | |||
}], '.foo{color:blue !important;}'); | |||
}); | |||
|
|||
it('does not mutate nested objects', () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be #226?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also #231
There was a problem hiding this comment.
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!
src/generate.js
Outdated
// value that was added by prefixAll. Let's try to figure out where it | ||
// goes. | ||
let originalStyle; | ||
if (elementNames[i][0] === 'W') { |
There was a problem hiding this comment.
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?
@@ -49,7 +49,6 @@ | |||
"caniuse-api": "^1.5.3", | |||
"chai": "^3.3.0", | |||
"coveralls": "^2.12.0", | |||
"es6-shim": "^0.35.3", |
There was a problem hiding this comment.
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.
@@ -369,11 +369,11 @@ describe('String handlers', () => { | |||
css(sheet.animate); | |||
flushToStyleTag(); | |||
|
|||
assertStylesInclude('@keyframes keyframe_1kmnkfo'); | |||
assertStylesInclude('@keyframes keyframe_tmjr6'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did this change?
There was a problem hiding this comment.
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
} | ||
} | ||
|
||
set(key /* : string */, value /* : any */) { | ||
if (!this.elements.hasOwnProperty(key)) { | ||
this.keyOrder.push(key); | ||
} | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
stringHandlers, useImportant); | ||
|
||
injectGeneratedCSSOnce(key, generated); | ||
if (alreadyInjected[key]) { |
There was a problem hiding this comment.
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. :)
package.json
Outdated
"npm-run-all": "^1.7.0", | ||
"nyc": "^6.4.4", | ||
"rimraf": "^2.5.2", | ||
"webpack": "^1.12.2" | ||
}, | ||
"dependencies": { | ||
"asap": "^2.0.3", | ||
"can-use-dom": "^0.1.0", |
There was a problem hiding this comment.
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.
src/prefix.js
Outdated
|
||
// We need keepUnprefixed so our sorting code knows how to order the | ||
// prefixed styles. | ||
const prefixer = new Prefixer({ |
There was a problem hiding this comment.
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?
src/inject.js
Outdated
key, [val[key]], selectorHandlers, stringHandlers, false); | ||
}); | ||
|
||
if (val instanceof OrderedElements) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/generate.js
Outdated
// value that was added by prefixAll. Let's try to figure out where it | ||
// goes. | ||
let originalStyle; | ||
if (elementNames[i][0] === 'W') { |
There was a problem hiding this comment.
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?
src/inject.js
Outdated
key, [val[key]], selectorHandlers, stringHandlers, false); | ||
}); | ||
|
||
if (val instanceof OrderedElements) { |
There was a problem hiding this comment.
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.
} | ||
} | ||
|
||
set(key /* : string */, value /* : any */) { | ||
if (!this.elements.hasOwnProperty(key)) { | ||
this.keyOrder.push(key); | ||
} | ||
|
There was a problem hiding this comment.
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.
src/prefix.js
Outdated
|
||
// We need keepUnprefixed so our sorting code knows how to order the | ||
// prefixed styles. | ||
const prefixer = new Prefixer({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
@@ -369,11 +369,11 @@ describe('String handlers', () => { | |||
css(sheet.animate); | |||
flushToStyleTag(); | |||
|
|||
assertStylesInclude('@keyframes keyframe_1kmnkfo'); | |||
assertStylesInclude('@keyframes keyframe_tmjr6'); |
There was a problem hiding this comment.
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
a54fd13
to
893386a
Compare
Once #233 is merged, I'll rebase this again and it will be only 2 commits. |
893386a
to
7831e48
Compare
Thanks @lencioni 🍻 Any chance someone from Khan can review it ? (@xymostech) This PR fixes #231. |
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.
7831e48
to
0700a57
Compare
There was a problem hiding this comment.
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.
// 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) { |
There was a problem hiding this comment.
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?
this looks great I support this |
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 Just gave you access! The version bump should happen with |
Great! I should have some time this afternoon to take care of this. |
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:
After:
@xymostech