customElements.define()应在组件使用场景对齐时调用:首屏组件在defer脚本中同步定义,非首屏组件在用户交互时动态import并注册,避免全局提前注册导致内存占用和首屏阻塞。

customElements.define() 该在什么时候调用
不能一加载脚本就全部注册,否则白占内存、拖慢首屏。定义时机必须和使用场景对齐。
- 首屏可能用到的组件(比如
<main-header>),放到<script type="module" defer>里同步定义,确保 DOM 解析完就能升级 - 非首屏组件(比如
<settings-dialog>、<analytics-chart>),等用户点击按钮再动态 define:button.addEventListener('click', async () => { const { default: SettingsDialog } = await import('./settings-dialog.js'); customElements.define('settings-dialog', SettingsDialog); }); - 绝对不要在全局作用域顶部写一堆
customElements.define()——尤其当模块依赖未就绪或含大量模板时,会卡住渲染主线程
组件内容怎么真正“按需拉取”,而不是只隐藏
靠 display: none 或 hidden 属性不是按需加载,只是视觉隐藏;真正的按需,是首次连接 DOM 时才发起请求、解析、渲染。
- 在
connectedCallback()里触发加载,不是constructor()—— 此时this.shadowRoot还为null - 用
data-src存 API 路径,配合IntersectionObserver控制时机:new IntersectionObserver(cb, { rootMargin: '50px' }),元素进入视口前 50px 才开始 fetch - 加载完成后立刻调用
observer.unobserve(this),否则路由切换后 observer 仍存活,引发重复请求和内存泄漏
如何让加载失败可恢复、状态可持久
用户点开侧边栏却卡住、刷新后回到收起状态、服务端渲染和客户端不一致——这些问题都源于状态没落地、错误没兜底。
- 用
data-collapsed="true"这类自定义属性存状态,CSS 和 JS 都能读,也兼容 SSR;别只靠 JS 变量 - fetch 失败后,加一个重试按钮并绑定
click事件,而不是静默失败或无限轮询 - 服务端返回的 HTML 片段必须过滤掉
<script>标签——否则客户端插入时不会执行,还可能 XSS;可信源才考虑手动解析 script 并动态创建
shadowRoot 封装 vs 样式继承,怎么选
不是所有组件都适合 shadow DOM;它隔离强,但也切断了外部样式流,比如主题色、字体继承会失效。
立即学习“前端免费学习笔记(深入)”;
- 纯功能型组件(如
<date-picker>)推荐用this.attachShadow({ mode: 'open' }),避免样式污染 - 需要响应全局主题的组件(如
<card>),改用:host或part暴露钩子:<style>:host { --card-bg: var(--bg-color); }</style>,让外部 CSS 能穿透控制 - 若组件内要复用页面级 CSS 类(如
text-sm、bg-primary),别用 shadowRoot,直接挂载到 light DOM,靠 class 名约定来管理
connectedCallback 里没做 loading 状态标记,用户点了没反馈;或者 import() 失败没 catch,导致整个模块不可用。



















