Created
July 3, 2025 16:52
-
-
Save tkh44/6b47c86813ce38de23f54810374defd1 to your computer and use it in GitHub Desktop.
Tiny CSS in JS library with new hashing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Types | |
export type CSSValue = string | number | (string | number)[] | |
export type CSSObject = { | |
[key: string]: CSSValue | CSSObject | |
} | |
// Cache for deduplication | |
const styleCache = new Map<string, string>() | |
const insertedRules = new Set<string>() | |
// --------------------------------------------------------------------------------- | |
// Sheet abstraction (constructable-sheet fast-path + micro-task batching) | |
// --------------------------------------------------------------------------------- | |
class Sheet { | |
private sheet: CSSStyleSheet | |
private queue: string[] = [] | |
private scheduled = false | |
constructor () { | |
if (typeof document === 'undefined') { | |
throw new Error('Razor: document is not defined.') | |
} | |
// Prefer constructable sheets when available (Chrome/Edge) | |
if ((document as any).adoptedStyleSheets && 'replaceSync' in CSSStyleSheet.prototype) { | |
this.sheet = new CSSStyleSheet() | |
;(document as any).adoptedStyleSheets = [...(document as any).adoptedStyleSheets, this.sheet] | |
} else { | |
const el = document.createElement('style') | |
el.setAttribute('data-razor', '') | |
document.head.appendChild(el) | |
this.sheet = el.sheet as CSSStyleSheet | |
} | |
} | |
insert (rule: string) { | |
this.queue.push(rule) | |
if (!this.scheduled) { | |
this.scheduled = true | |
queueMicrotask(() => this.flush()) | |
} | |
} | |
private flush () { | |
if ('replaceSync' in this.sheet) { | |
// Constructable sheet – single syscall, super fast | |
;(this.sheet as any).replaceSync(this.sheet.cssRules.length ? (this.sheet as any).cssRules[0].cssText + this.queue.join('') : this.queue.join('')) | |
} else { | |
const s = this.sheet as CSSStyleSheet | |
for (const r of this.queue) { | |
try { s.insertRule(r, s.cssRules.length) } catch {} | |
} | |
} | |
this.queue.length = 0 | |
this.scheduled = false | |
} | |
} | |
// Lazy-initialize sheet on first use | |
let sheet: Sheet | null = null | |
function getSheet(): Sheet { | |
if (!sheet) { | |
sheet = new Sheet() | |
} | |
return sheet | |
} | |
// Export for testing purposes only | |
export function __resetSheet() { | |
sheet = null | |
styleCache.clear() | |
insertedRules.clear() | |
keyframesCache.clear() | |
} | |
// Keyframe tracking | |
const keyframesCache = new Set<string>() | |
// --------------------------------------------------------------------------------- | |
// WeakMap identity cache for style objects → hashKey | |
const objectHashCache = new WeakMap<object, string>() | |
// Fragment cache: stores hashes for individual "key:value" pairs | |
// This acts like a memoization table for style fragments we've seen before | |
// Example: "padding:20" → 0x12345678 (some 32-bit hash) | |
const fragmentCache = new Map<string, number>() | |
// ---------- FNV-1a 32-bit hash (base-36 string) ---------- | |
function fnv1a (str: string): string { | |
let h = 0x811c9dc5 | |
for (let i = 0, l = str.length; i < l; i++) { | |
h ^= str.charCodeAt(i) | |
h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0 | |
} | |
return 'r' + h.toString(36) | |
} | |
// FNV-1a that returns raw 32-bit integer (for fragment cache) | |
function fnv1aInt(str: string): number { | |
// Start with FNV offset basis (a magic prime number) | |
let h = 0x811c9dc5 | |
// For each character in the string | |
for (let i = 0, l = str.length; i < l; i++) { | |
// XOR the hash with the character code | |
h ^= str.charCodeAt(i) | |
// Multiply by FNV prime (16777619) using bit shifts for speed | |
// This spreads the bits around for better distribution | |
h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0 | |
} | |
return h | |
} | |
// Mix two hashes together to create a new hash | |
// This ensures that {a:1, b:2} produces a different hash than {b:2, a:1} | |
function mixHashes(hash1: number, hash2: number): number { | |
// XOR combines the bits from both hashes | |
// Multiply by golden ratio prime (0x9e3779b9) for better bit diffusion | |
// This constant comes from floor(2^32 / φ) where φ is golden ratio | |
return ((hash1 ^ hash2) * 0x9e3779b9) >>> 0 | |
} | |
// Fast object hashing with fragment caching | |
function hashObject(obj: CSSObject): string { | |
// Step 1: Check if we've already computed the hash for this exact object instance | |
const cached = objectHashCache.get(obj) | |
if (cached) return cached | |
// Step 2: Start computing the hash using fragment caching | |
const hashInt = hashObjectInt(obj) | |
// Step 3: Convert the 32-bit integer to a base-36 string (shorter than base-10) | |
const hashStr = 'r' + hashInt.toString(36) | |
// Step 4: Cache the result for this object instance | |
objectHashCache.set(obj, hashStr) | |
return hashStr | |
} | |
// Internal function that returns hash as integer (for recursive calls) | |
function hashObjectInt(obj: CSSObject): number { | |
// Initialize with FNV offset basis | |
let hash = 0x811c9dc5 | |
// Get all keys - we rely on insertion order (stable in ES2015+) | |
const keys = Object.keys(obj) | |
// Process each property | |
for (let i = 0, len = keys.length; i < len; i++) { | |
const key = keys[i] | |
const value = obj[key] | |
// Skip null/undefined values | |
if (value == null) continue | |
if (typeof value === 'object' && !Array.isArray(value)) { | |
// Case 1: Nested object (like { '&:hover': { color: 'red' } }) | |
// Recursively hash the nested object | |
const nestedHash = hashObjectInt(value) | |
// Mix the key into our hash | |
for (let j = 0; j < key.length; j++) { | |
hash ^= key.charCodeAt(j) | |
hash = (hash + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0 | |
} | |
// Mix the nested object's hash into our hash | |
hash = mixHashes(hash, nestedHash) | |
} else { | |
// Case 2: Simple property (like { padding: 20 }) | |
// Build the fragment string "key:value" | |
const valueStr = Array.isArray(value) ? value.join(',') : String(value) | |
const fragment = key + ':' + valueStr | |
// Check if we've already hashed this fragment | |
let fragmentHash = fragmentCache.get(fragment) | |
if (fragmentHash === undefined) { | |
// First time seeing this fragment - compute and cache its hash | |
fragmentHash = fnv1aInt(fragment) | |
fragmentCache.set(fragment, fragmentHash) | |
// Optional: Log cache growth in dev | |
// if (fragmentCache.size % 100 === 0) { | |
// console.log(`Fragment cache size: ${fragmentCache.size}`) | |
// } | |
} | |
// Mix this fragment's hash into our running hash | |
// The order matters! {a:1, b:2} will hash differently than {b:2, a:1} | |
hash = mixHashes(hash, fragmentHash) | |
} | |
} | |
return hash | |
} | |
// Serialize object styles to CSS string (regex-free) | |
function serializeStyles( | |
styles: CSSObject, | |
selector: string = '', | |
extractedKeyframes: string[] = [], | |
rules: string[] = [] | |
): { rules: string[]; keyframes: string[] } { | |
let css = '' | |
for (const key in styles) { | |
const value = styles[key] | |
// --- Nested structures ---------------------------------------- | |
if (typeof value === 'object' && !Array.isArray(value)) { | |
if (key.startsWith('@keyframes')) { | |
// Build keyframes rule | |
const keyframeName = key.split(' ')[1] | |
let keyframeRules = '' | |
for (const frame in value) { | |
const frameStyles = value[frame] as CSSObject | |
let frameCss = '' | |
for (const prop in frameStyles) { | |
const cssProp = prop.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`) | |
frameCss += `${cssProp}:${frameStyles[prop]};` | |
} | |
keyframeRules += `${frame}{${frameCss}}` | |
} | |
extractedKeyframes.push(`@keyframes ${keyframeName}{${keyframeRules}}`) | |
} else if (key.startsWith('@media') || key.startsWith('@supports')) { | |
serializeStyles(value, selector, extractedKeyframes, rules) | |
// Wrap last N rules inside media | |
const spanStart = rules.length - 1 | |
const inner = rules.splice(spanStart, rules.length - spanStart).join('') | |
rules.push(`${key}{${inner}}`) | |
} else { | |
// Handle :global() syntax | |
if (key.startsWith(':global(') && key.endsWith(')')) { | |
const globalSel = key.slice(8, -1) // Remove :global( and ) | |
serializeStyles(value, globalSel, extractedKeyframes, rules) | |
} else { | |
const nestedSel = key.includes('&') ? key.replace(/&/g, selector) : `${selector} ${key}` | |
serializeStyles(value, nestedSel, extractedKeyframes, rules) | |
} | |
} | |
} else if (value != null) { | |
// --- Plain property (ignore null/undefined) ---------------------------------------- | |
const cssProp = key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`) | |
if (Array.isArray(value)) { | |
for (let i = 0; i < value.length; i++) css += `${cssProp}:${value[i]};` | |
} else { | |
css += `${cssProp}:${value};` | |
} | |
} | |
} | |
if (css) { | |
rules.push(selector ? `${selector}{${css}}` : css) | |
} | |
return { rules, keyframes: extractedKeyframes } | |
} | |
// Flatten arguments recursively | |
function flattenArgs(args: any[]): CSSObject[] { | |
const result: CSSObject[] = [] | |
for (const arg of args) { | |
if (!arg) continue | |
if (Array.isArray(arg)) { | |
result.push(...flattenArgs(arg)) | |
} else { | |
result.push(arg) | |
} | |
} | |
return result | |
} | |
// Main css function | |
export function css(...args: any[]): string { | |
// Flatten and filter out falsy values | |
const flattened = flattenArgs(args) | |
// Fast path for single object | |
if (flattened.length === 1) { | |
const single = flattened[0] | |
const cacheKey = hashObject(single) | |
// Check cache | |
let className = styleCache.get(cacheKey) | |
if (className) return className | |
// Generate class name | |
className = fnv1a(cacheKey) | |
// Generate and insert CSS | |
const { rules, keyframes } = serializeStyles(single, `.${className}`) | |
insertStyles(rules, keyframes) | |
// Cache and return | |
styleCache.set(cacheKey, className) | |
return className | |
} | |
// Multiple objects - merge them | |
const merged: CSSObject = {} | |
// More efficient merging for multiple objects | |
for (const obj of flattened) { | |
for (const key in obj) { | |
const value = obj[key] | |
// Handle nested objects (like &:hover) | |
if (typeof value === 'object' && !Array.isArray(value) && | |
typeof merged[key] === 'object' && !Array.isArray(merged[key])) { | |
// Merge nested objects | |
merged[key] = { ...(merged[key] as CSSObject), ...(value as CSSObject) } | |
} else { | |
merged[key] = value | |
} | |
} | |
} | |
// Generate cache key from merged object | |
const cacheKey = hashObject(merged) | |
// Check cache | |
let className = styleCache.get(cacheKey) | |
if (className) return className | |
// Generate class name | |
className = fnv1a(cacheKey) | |
// Generate and insert CSS | |
const { rules, keyframes } = serializeStyles(merged, `.${className}`) | |
insertStyles(rules, keyframes) | |
// Cache and return | |
styleCache.set(cacheKey, className) | |
return className | |
} | |
// Helper to insert styles | |
function insertStyles(rules: string[], keyframes: string[]) { | |
for (const kf of keyframes) { | |
if (!keyframesCache.has(kf)) { | |
getSheet().insert(kf) | |
keyframesCache.add(kf) | |
} | |
} | |
for (const rule of rules) { | |
if (!insertedRules.has(rule)) { | |
getSheet().insert(rule) | |
insertedRules.add(rule) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment