Skip to content

Commit 11a5936

Browse files
committed
perf: adopt FxHashMap/FxHashSet for hot-path hash collections
Replace std HashMap/HashSet with rustc-hash FxHashMap/FxHashSet across extractor, sheet, and wasm crates. FxHash uses a faster non-cryptographic hash (~50% faster for short string keys like CSS properties) which is safe for build-time processing of trusted source code. Converted: CollectedStyles (11 fields), DevupVisitor (4 fields), ExtractOutput.styles, remap_style_names, extract_class_map_from_code, class_mapping utilities, expression_map, PropertyMap, and update_styles. Kept std HashMap for ExtractOption.import_aliases (public API, cold path, WASM bindings compatibility). All 1,702 tests passing. Zero clippy warnings.
1 parent a705c01 commit 11a5936

12 files changed

Lines changed: 100 additions & 91 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/devup-ui-wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ wasm-bindgen = "0.2.113"
1919
extractor = { path = "../../libs/extractor" }
2020
sheet = { path = "../../libs/sheet" }
2121
css = { path = "../../libs/css" }
22+
rustc-hash = "2"
2223

2324
# The `console_error_panic_hook` crate provides better debugging of panics by
2425
# logging them with `console.error`. This is great for development, but requires

bindings/devup-ui-wasm/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use css::class_map::{get_class_map, set_class_map};
22
use css::file_map::{get_file_map, get_filename_by_file_num, set_file_map};
33
use extractor::extract_style::extract_style_value::ExtractStyleValue;
44
use extractor::{ExtractOption, ImportAlias, extract, has_devup_ui};
5+
use rustc_hash::FxHashSet;
56
use sheet::StyleSheet;
6-
use std::collections::{HashMap, HashSet};
7+
use std::collections::HashMap;
78
use std::sync::{LazyLock, Mutex};
89
use wasm_bindgen::prelude::*;
910

@@ -34,7 +35,7 @@ pub struct Output {
3435
impl Output {
3536
fn new(
3637
code: String,
37-
styles: HashSet<ExtractStyleValue>,
38+
styles: FxHashSet<ExtractStyleValue>,
3839
map: Option<String>,
3940
single_css: bool,
4041
filename: String,
@@ -739,7 +740,7 @@ mod tests {
739740
css::class_map::reset_class_map();
740741

741742
// Create output with empty styles
742-
let styles = HashSet::new();
743+
let styles = FxHashSet::default();
743744
let output = Output::new(
744745
"code".to_string(),
745746
styles,
@@ -839,7 +840,7 @@ mod tests {
839840
css::class_map::reset_class_map();
840841

841842
// Now create output which should trigger rm_global_css
842-
let styles = HashSet::new();
843+
let styles = FxHashSet::default();
843844
let output = Output::new(
844845
"new code".to_string(),
845846
styles,

libs/extractor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ strum = "0.28.0"
1919
strum_macros = "0.28.0"
2020
serde_json = "1.0"
2121
boa_engine = "0.21"
22+
rustc-hash = "2"
2223

2324
[dev-dependencies]
2425
insta = "1.46.3"

libs/extractor/src/css_utils.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ pub fn css_to_style_literal<'a>(
6262
// Build a combined CSS string with unique placeholders for expressions
6363
// Use a format that won't conflict with actual CSS values
6464
let mut css_parts = Vec::new();
65-
let mut expression_map = std::collections::HashMap::with_capacity(css.expressions.len());
65+
let mut expression_map =
66+
rustc_hash::FxHashMap::with_capacity_and_hasher(css.expressions.len(), Default::default());
6667

6768
for (i, quasi) in css.quasis.iter().enumerate() {
6869
css_parts.push(quasi.value.raw.to_string());

libs/extractor/src/extractor/extract_style_from_styled.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashMap;
1+
use rustc_hash::FxHashMap;
22

33
use crate::{
44
ExtractStyleProp,
@@ -19,7 +19,7 @@ use oxc_span::SPAN;
1919

2020
fn extract_base_tag_and_class_name<'a>(
2121
input: &Expression<'a>,
22-
imports: &HashMap<String, ExportVariableKind>,
22+
imports: &FxHashMap<String, ExportVariableKind>,
2323
) -> (Option<String>, Option<Vec<ExtractStyleValue>>) {
2424
if let Expression::StaticMemberExpression(member) = input {
2525
(Some(member.property.name.to_string()), None)
@@ -58,7 +58,7 @@ pub fn extract_style_from_styled<'a>(
5858
ast_builder: &AstBuilder<'a>,
5959
expression: &mut Expression<'a>,
6060
split_filename: Option<&str>,
61-
imports: &HashMap<String, ExportVariableKind>,
61+
imports: &FxHashMap<String, ExportVariableKind>,
6262
) -> (ExtractResult<'a>, Expression<'a>) {
6363
let (result, new_expr) = if let Expression::TaggedTemplateExpression(tag) = expression
6464
&& let (Some(tag_name), default_class_name) =

libs/extractor/src/lib.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use oxc_ast_visit::VisitMut;
2121
use oxc_codegen::{Codegen, CodegenOptions};
2222
use oxc_parser::{Parser, ParserReturn};
2323
use oxc_span::SourceType;
24-
use std::collections::{BTreeMap, HashMap, HashSet};
24+
use rustc_hash::{FxHashMap, FxHashSet};
25+
use std::collections::{BTreeMap, HashMap};
2526
use std::error::Error;
2627
use std::path::PathBuf;
2728

@@ -131,7 +132,7 @@ impl<'a> ExtractStyleProp<'a> {
131132
#[derive(Debug)]
132133
pub struct ExtractOutput {
133134
// used styles
134-
pub styles: HashSet<ExtractStyleValue>,
135+
pub styles: FxHashSet<ExtractStyleValue>,
135136

136137
// output source
137138
pub code: String,
@@ -182,7 +183,7 @@ pub fn extract(
182183
if !has_relevant_import {
183184
// skip if not using package
184185
return Ok(ExtractOutput {
185-
styles: HashSet::new(),
186+
styles: FxHashSet::default(),
186187
code: code.to_string(),
187188
map: None,
188189
css_file: None,
@@ -216,7 +217,7 @@ pub fn extract(
216217
let class_map = if !partial_code.is_empty() {
217218
extract_class_map_from_code(filename, &partial_code, &option, &referenced)?
218219
} else {
219-
std::collections::HashMap::new()
220+
FxHashMap::default()
220221
};
221222

222223
// Generate full code with class names substituted into selectors
@@ -240,7 +241,7 @@ pub fn extract(
240241
// For vanilla-extract files, if no styles were collected, return early
241242
if is_vanilla_extract && processed_code.is_empty() {
242243
return Ok(ExtractOutput {
243-
styles: HashSet::new(),
244+
styles: FxHashSet::default(),
244245
code: code.to_string(),
245246
map: None,
246247
css_file: None,
@@ -310,8 +311,8 @@ fn extract_class_map_from_code(
310311
filename: &str,
311312
partial_code: &str,
312313
option: &ExtractOption,
313-
style_names: &HashSet<String>,
314-
) -> Result<std::collections::HashMap<String, String>, Box<dyn Error>> {
314+
style_names: &FxHashSet<String>,
315+
) -> Result<FxHashMap<String, String>, Box<dyn Error>> {
315316
let source_type = SourceType::from_path(filename)?;
316317
let css_file = if option.single_css {
317318
format!("{}/devup-ui.css", option.css_dir)
@@ -331,7 +332,7 @@ fn extract_class_map_from_code(
331332
..
332333
} = Parser::new(&allocator, partial_code, source_type).parse();
333334
if panicked {
334-
Ok(std::collections::HashMap::new())
335+
Ok(FxHashMap::default())
335336
} else {
336337
let mut visitor = DevupVisitor::new(
337338
&allocator,
@@ -350,7 +351,7 @@ fn extract_class_map_from_code(
350351

351352
// Parse the output code to extract class name assignments
352353
// Format: const styleName = "className" or const styleName = "className1 className2"
353-
let mut class_map = std::collections::HashMap::new();
354+
let mut class_map = FxHashMap::default();
354355
for line in result.code.lines() {
355356
let line = line.trim();
356357
if line.starts_with("const ") || line.starts_with("export const ") {
@@ -13315,7 +13316,7 @@ export const card = style({
1331513316
#[serial]
1331613317
fn test_extract_class_map_from_code_parser_panic() {
1331713318
// Test extract_class_map_from_code with invalid code that causes parser panic (covers line 153-154)
13318-
let mut style_names = HashSet::new();
13319+
let mut style_names = FxHashSet::default();
1331913320
style_names.insert("test".to_string());
1332013321

1332113322
let result = extract_class_map_from_code(

libs/extractor/src/prop_modify_utils.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use oxc_ast::ast::{
1313
PropertyKey, PropertyKind, TemplateElementValue,
1414
};
1515
use oxc_span::SPAN;
16-
use std::collections::HashMap;
16+
use rustc_hash::FxHashMap;
1717

1818
/// Combine two optional className expressions into a conditional expression.
1919
/// `condition ? con_expr : alt_expr`, falling back to `""` for the missing branch.
@@ -428,8 +428,8 @@ fn build_tailwind_class_mapping(
428428
class_str: &str,
429429
style_order: Option<u8>,
430430
filename: Option<&str>,
431-
) -> HashMap<String, String> {
432-
let mut mapping = HashMap::new();
431+
) -> FxHashMap<String, String> {
432+
let mut mapping = FxHashMap::default();
433433

434434
for class in class_str.split_whitespace() {
435435
if let Some(parsed) = parse_single_class(class) {
@@ -451,7 +451,7 @@ fn build_tailwind_class_mapping(
451451
fn rebuild_template_literal_with_mapping<'a>(
452452
ast_builder: &AstBuilder<'a>,
453453
template: &oxc_ast::ast::TemplateLiteral<'a>,
454-
class_mapping: &HashMap<String, String>,
454+
class_mapping: &FxHashMap<String, String>,
455455
) -> Expression<'a> {
456456
// Rebuild quasis with replaced class names
457457
let new_quasis = template.quasis.iter().map(|quasi| {
@@ -486,7 +486,7 @@ fn rebuild_template_literal_with_mapping<'a>(
486486
}
487487

488488
/// Replace Tailwind class names in a string with generated class names
489-
fn replace_classes_in_string(s: &str, class_mapping: &HashMap<String, String>) -> String {
489+
fn replace_classes_in_string(s: &str, class_mapping: &FxHashMap<String, String>) -> String {
490490
let mut result = s.to_string();
491491
// Sort by length descending to avoid partial replacements (e.g., "text-3xl" before "text-3")
492492
let mut sorted_classes: Vec<_> = class_mapping.iter().collect();
@@ -502,7 +502,7 @@ fn replace_classes_in_string(s: &str, class_mapping: &HashMap<String, String>) -
502502
fn rebuild_expression_with_mapping<'a>(
503503
ast_builder: &AstBuilder<'a>,
504504
expr: &Expression<'a>,
505-
class_mapping: &HashMap<String, String>,
505+
class_mapping: &FxHashMap<String, String>,
506506
) -> Expression<'a> {
507507
match expr {
508508
Expression::StringLiteral(lit) => {

0 commit comments

Comments
 (0)