Skip to content

Instantly share code, notes, and snippets.

@tkh44
Created July 3, 2025 16:52
Show Gist options
  • Save tkh44/6b47c86813ce38de23f54810374defd1 to your computer and use it in GitHub Desktop.
Save tkh44/6b47c86813ce38de23f54810374defd1 to your computer and use it in GitHub Desktop.
Tiny CSS in JS library with new hashing
// 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