contenteditable 卡顿主因是频繁强制 layout:每次输入、光标移动或读取 textcontent 都触发同步布局,单次超 8ms;错误操作如 input 中调用 getboundingclientrect、父容器未设高度、:focus 含 box-shadow 等会加剧问题;ios 更严重,软键盘+ime+光标定位叠加易致 layout thrashing。

contenteditable 一输大段就卡,不是 DOM 太大,是 layout 被反复强制触发
浏览器对 contenteditable 元素的渲染策略很激进:每次按键、光标移动、粘贴、甚至只是读一次 el.textContent,都可能触发同步 layout + paint。单次 layout 耗时常超 8ms,60fps 下已超标;连续输入下主线程持续阻塞,帧率直接掉到 30fps 以下。
常见错误现象:input 回调里调用 el.innerHTML 或 getBoundingClientRect();父容器用了 flex 或 grid 却没设明确高度;CSS 中 :focus 里带 box-shadow 或 transform。
- 别信“DOM 少就安全”——一个带复杂 CSS 的
<div contenteditable="true"> 比 100 个纯文本 <code><p></p>更容易卡 - iOS 更糟:软键盘弹出 + IME 输入状态 + 光标定位三者叠加,极易触发 layout thrashing
- 空内容时
scrollHeight在 Safari 返回 0 或 1px,导致高度塌陷,不是 bug,是实现差异 - 必须显式加
tabindex="0",tabindex="-1"只支持 JS 主动聚焦,不进 Tab 链 - 若父容器有
overflow: hidden且内容溢出,tabindex="0"可能引发滚动跳变,可加scroll-behavior: smooth缓解 - 移动端需在
touchstart后立即el.focus(),不要等click(有 300ms 延迟) - 优先走纯文本路径:
e.clipboardData.getData('text/plain'),插入成本极低 - 需保留基础语义时,先用
DOMPurify.sanitize(html, {ALLOWED_TAGS: ['b','i','u','p','br']})过滤,再插入——比全量解析快 3–5 倍 - 绝对禁止
el.innerHTML = e.clipboardData.getData('text/html'),这是 XSS 和性能双坑 - 监听
paste时必须e.preventDefault(),否则默认行为会和你手动插入竞争,造成 DOM 不一致 - 先
el.style.height = 'auto'重置,再赋el.style.height = el.scrollHeight + 'px' - 空内容时兜底:
el.textContent.trim().length === 0判断,设最小高度如1.2em - 包一层
requestAnimationFrame,避免连续input触发多次 layout —— 尤其粘贴大段文本时 - 禁用
overflow-y: scroll,改用overflow-y: hidden,否则滚动条宽度变化会再次触发 layout
为什么加了 tabindex="0" 还不能稳定聚焦
contenteditable="true" 本身不提供焦点能力。没配 tabindex="0",等于开关没打开:用户点鼠标能输,但 Tab 切不进去、el.focus() 不生效、document.execCommand 全部静默,因为 Selection API 根本拿不到 range。
典型表现:Ctrl+B 没反应、回车后光标消失、粘贴后结构错乱。旧版 Safari 对缺失 tabindex 更敏感,点击后光标闪一下就消失。
paste 事件里取 text/html 是性能毒药,不是 XSS 问题才要拦
直接用 e.clipboardData.getData('text/html') 插入,等于把整个 HTML 解析、样式计算、布局树重建流程重跑一遍。Word 粘贴常含几十层嵌套、style 属性、data-* 标记,DOM 操作耗时陡增,移动端极易卡顿甚至主线程阻塞。
scrollHeight 动态设高要绕开三个坑
scrollHeight 是目前最可靠的自适应依据,但它在 Safari 空内容时返回值偏小,且频繁赋值 el.style.height 会连续触发 layout。
tabindex="0" 导致焦点链断裂,又在 input 里读 innerHTML,再配合 :focus 动画和粘贴 text/html —— 这四件事凑一块,再轻量的编辑器也会在用户连续输入 3 秒后开始掉帧。











