BEM核心是用单类名将模块边界写进类名本身,不依赖工具或运行时:.user-card__avatar直接表明归属、角色与状态;单类名支持浏览器一次哈希查找,避免嵌套选择器DOM回溯导致的性能暴跌。

在 CSS 模块化开发中推荐使用 BEM 命名选择器,核心原因不是它“看起来规范”,而是它用最轻量的方式把模块边界写进类名本身——不依赖构建工具、不靠 JS 运行时、也不需要框架支持,就能让 .user-card__avatar 这个字符串自己说出三件事:这是谁的(user-card)、是什么(__avatar)、当前什么状态(可加 --loading)。
为什么单类名选择器是模块化的性能底线
BEM 强制只用单类名(如 .user-card__avatar),浏览器匹配时直接查 class 哈希表,一次命中;而 .user-card .avatar 这种写法会让浏览器先找所有 .avatar 元素,再逐层往上检查父级是否含 .user-card,DOM 越深越慢。这不是“可能变慢”,在低端 Android 设备上,≥4 层嵌套的回溯式匹配,Recalculate Style 耗时可能高出 4 倍。
验证方法很简单:
– 打开 Chrome DevTools → Elements → 选中元素 → Computed → Styles 面板,看每条规则是不是纯 .xxx,有没有空格、div 或 [data-]
– 构建后跑 grep -r "\.[a-z]\+ \.[a-z]" dist/,揪出所有残留的空格选择器
– Sass 中只允许 &__avatar,禁用 & .user-card__badge 这类写法
为什么 BEM 类名天然对应组件文件结构
一个 UserCard.vue 组件,其目录结构、文件名、CSS 块名三者一致:/components/user-card/UserCard.module.css 里只该出现 .user-card 和 .user-card__avatar。这种映射不是巧合,是设计使然。
立即学习“前端免费学习笔记(深入)”;
- 常见错误:
.card .header这类嵌套选择器一挪 DOM(比如加个 wrapper)就失效;而.user-card__header天然绑定宿主块,结构无关 - Element 必须是 Block 的直接子角色:
.dialog__title✅,.dialog__header__title❌(BEM 不允许双下划线嵌套) - 工具链必须校验:用
stylelint-selector-bem-pattern规则设为{"componentName": "[a-z][a-zA-Z0-9]+", "styleType": "bem"},CI 阶段拦截.ui-input__field出现在user-card.css中这类越界问题
为什么修饰符(--)和响应式逻辑必须收敛到 BEM 类上
响应式不是“加个 @media 就完事”,而是要明确变化范围和归属。BEM 把这个决策显式编码进类名:
- 整块结构切换(如网格变列表)→ 用 Block 修饰符:
.product-list--stacked - 仅元素自身微调(如图标右对齐)→ 用 Element 修饰符:
.button__icon--right - 禁止写
.product-card .price配合媒体查询——这种选择器无法被stylelint-selector-bem-pattern拦截,且一挪 DOM 就失效 - 修饰符名要表达意图,而非设备:
--responsive-grid✅,--on-tablet❌
真正难的不是写对一个类名,而是每次敲下 __ 或 -- 时都得问自己:这个元素是不是它所属 Block 的固有部分?这个修饰符是不是可枚举、可复用、不带硬编码值的状态?这种判断没法自动化,也最容易在赶工期时被跳过。


















