Skip to content

Commit

Permalink
Minify combined class names (#248)
Browse files Browse the repository at this point in the history
* Minify combined class names

* rmv underscare from _name in production

* hashString

* avoid re-hashing in production

* fix re-hash logic and add re-hash spec

* hashObject use hashString

* add _len to StyleSheet definition and use it in getStyleDefinitionsLengthHash to add extra hash bit in prod

* improve comment
  • Loading branch information
gilbox authored and xymostech committed Jul 19, 2017
1 parent 88e5926 commit 7f9f602
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 9 deletions.
7 changes: 4 additions & 3 deletions src/exports.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow */
import {mapObj, hashObject} from './util';
import {mapObj, hashString} from './util';
import {
injectAndGetClassName,
reset, startBuffering, flushToString,
Expand All @@ -20,10 +20,11 @@ export type MaybeSheetDefinition = SheetDefinition | false | null | void
const StyleSheet = {
create(sheetDefinition /* : SheetDefinition */) {
return mapObj(sheetDefinition, ([key, val]) => {
const stringVal = JSON.stringify(val);
return [key, {
// TODO(gil): Further minify the -O_o--combined hashes
_len: stringVal.length,
_name: process.env.NODE_ENV === 'production' ?
`_${hashObject(val)}` : `${key}_${hashObject(val)}`,
hashString(stringVal) : `${key}_${hashString(stringVal)}`,
_definition: val
}];
});
Expand Down
20 changes: 18 additions & 2 deletions src/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import asap from 'asap';

import OrderedElements from './ordered-elements';
import {generateCSS} from './generate';
import {hashObject} from './util';
import {hashObject, hashString} from './util';

/* ::
import type { SheetDefinition, SheetDefinitions } from './index.js';
Expand Down Expand Up @@ -243,6 +243,13 @@ const processStyleDefinitions = (
}
};

// Sum up the lengths of the stringified style definitions (which was saved as _len property)
// and use modulus to return a single byte hash value.
// We append this extra byte to the 32bit hash to decrease the chance of hash collisions.
const getStyleDefinitionsLengthHash = (styleDefinitions /* : any[] */) /* : string */ => (
styleDefinitions.reduce((length, styleDefinition) => length + styleDefinition._len, 0) % 36
).toString(36);

/**
* Inject styles associated with the passed style definition objects, and return
* an associated CSS class name.
Expand All @@ -269,7 +276,16 @@ export const injectAndGetClassName = (
if (processedStyleDefinitions.classNameBits.length === 0) {
return "";
}
const className = processedStyleDefinitions.classNameBits.join("-o_O-");

let className;
if (process.env.NODE_ENV === 'production') {
className = processedStyleDefinitions.classNameBits.length === 1 ?
`_${processedStyleDefinitions.classNameBits[0]}` :
`_${hashString(processedStyleDefinitions.classNameBits.join())}${
getStyleDefinitionsLengthHash(styleDefinitions)}`;
} else {
className = processedStyleDefinitions.classNameBits.join("-o_O-");
}

injectStyleOnce(
className,
Expand Down
6 changes: 4 additions & 2 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export const stringifyAndImportantifyValue = (
prop /* : any */
) /* : string */ => importantify(stringifyValue(key, prop));

// Turn a string into a hash string of base-36 values (using letters and numbers)
export const hashString = (string /* : string */) /* string */ => stringHash(string).toString(36);

// Hash a javascript object using JSON.stringify. This is very fast, about 3
// microseconds on my computer for a sample object:
// http://jsperf.com/test-hashfnv32a-hash/5
Expand All @@ -131,8 +134,7 @@ export const stringifyAndImportantifyValue = (
// this to produce consistent hashes browsers need to have a consistent
// ordering of objects. Ben Alpert says that Facebook depends on this, so we
// can probably depend on this too.
export const hashObject = (object /* : ObjectMap */) /* : string */ => stringHash(JSON.stringify(object)).toString(36);

export const hashObject = (object /* : ObjectMap */) /* : string */ => hashString(JSON.stringify(object));

// Given a single style value string like the "b" from "a: b;", adds !important
// to generate "b !important".
Expand Down
2 changes: 1 addition & 1 deletion tests/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ describe('StyleSheet.create', () => {
},
});

assert.equal(sheet.test._name, '_j5rvvh');
assert.equal(sheet.test._name, 'j5rvvh');
});
})
});
Expand Down
50 changes: 49 additions & 1 deletion tests/inject_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import jsdom from 'jsdom';

import { StyleSheet, css } from '../src/index.js';
import {
injectAndGetClassName,
injectStyleOnce,
reset, startBuffering, flushToString, flushToStyleTag,
addRenderedClassNames, getRenderedClassNames
addRenderedClassNames, getRenderedClassNames,
} from '../src/inject.js';
import { defaultSelectorHandlers } from '../src/generate';

const sheet = StyleSheet.create({
red: {
Expand All @@ -34,6 +36,52 @@ describe('injection', () => {
global.document = undefined;
});

describe('injectAndGetClassName', () => {
it('uses hashed class name', () => {
const className = injectAndGetClassName(false, [sheet.red], defaultSelectorHandlers);
assert.equal(className, 'red_137u7ef');
});

it('combines class names', () => {
const className = injectAndGetClassName(false, [sheet.red, sheet.blue, sheet.green], defaultSelectorHandlers);
assert.equal(className, 'red_137u7ef-o_O-blue_1tsdo2i-o_O-green_1jzdmtb');
});

describe('process.env.NODE_ENV === \'production\'', () => {
let prodSheet;
beforeEach(() => {
process.env.NODE_ENV = 'production';
prodSheet = StyleSheet.create({
red: {
color: 'red',
},

blue: {
color: 'blue',
},

green: {
color: 'green',
},
});
});

afterEach(() => {
delete process.env.NODE_ENV;
});

it('uses hashed class name (does not re-hash)', () => {
const className = injectAndGetClassName(false, [prodSheet.red], defaultSelectorHandlers);
assert.equal(className, `_${prodSheet.red._name}`);
});

it('creates minified combined class name', () => {
const className = injectAndGetClassName(false, [prodSheet.red, prodSheet.blue, prodSheet.green], defaultSelectorHandlers);
assert.equal(className, '_11v1eztc');
});
});
});

describe('injectStyleOnce', () => {
it('causes styles to automatically be added', done => {
injectStyleOnce("x", ".x", [{ color: "red" }], false);
Expand Down

0 comments on commit 7f9f602

Please sign in to comment.