You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chrome中window.getComputedStyle获取1ch元素宽度值异常问题

问题描述

我定义了一个单字符元素用于获取宽度:

<div ref={refBBFS} style={{width:"1ch",fontFamily:'some monospace font'}}>X</div>

环境为Next.js 13.1.1、React 18.2.0、Chrome 80.0.3987.132,遇到以下问题:

  • Chrome中,在根组件空依赖数组的useEffect里调用window.getComputedStyle(...).getPropertyValue('width')返回错误值(Firefox中正常运行)
  • 返回值是计算前的默认初始值:字号16(medium)时返回8px,字号12时返回6px,和Chrome DevTools显示的实际计算值(如9.63281px)不符
  • 在window.resize事件中调用该方法能得到预期值;修改代码触发Next.js热重载后,渲染时能获取正确值,但页面刷新(F5)后又返回错误值
  • 尝试过跳过首次渲染、手动触发resize事件等方案,问题仍未解决

示例代码:

export default function MyApp({ Component, pageProps }: AppPropsWithLayout)
{
    const refBBFS = useRef(null);
    useEffect(() =>
    {
        const BaseWidthPx =  window.getComputedStyle(refBBFS.current).getPropertyValue('width');
        const BaseFSPx    =  window.getComputedStyle(refBBFS.current).getPropertyValue('font-size');
        console.log('my app effect',BaseWidthPx,BaseFSPx);


        // add resize event handler
        function HandleResolutionChange()
       {

         console.log('** executing resolution change ** ',window.getComputedStyle(refBBFS.current).getPropertyValue('width'),window.getComputedStyle(refBBFS.current).getPropertyValue('font-size'));
        }
       window.addEventListener('resize', HandleResolutionChange);
       HandleResolutionChange();

       return () =>
       {
         window.removeEventListener('resize', HandleResolutionChange);

       }


    }, []);

return (<>
     <div ref={refBBFS} style={{width:"1ch",fontFamily:'...'}}>X</div>
</>);
}
原因分析

Chrome在页面首次加载时,对于使用ch这类相对字体单位的元素,可能在目标字体完全加载并完成渲染前就执行了空依赖的useEffect,此时浏览器会 fallback 到默认字体或者未完全初始化的字体度量值进行计算,导致得到错误的宽度。而热重载时字体已被缓存,resize事件触发时浏览器会重新计算布局,因此能拿到正确值。

解决方案

1. 等待字体加载完成后计算

利用document.fonts.ready确保所有字体加载完成后再获取计算样式:

useEffect(() => {
  const getBaseWidth = async () => {
    // 等待所有字体加载完成
    await document.fonts.ready;
    if (!refBBFS.current) return;
    
    const BaseWidthPx = window.getComputedStyle(refBBFS.current).getPropertyValue('width');
    const BaseFSPx = window.getComputedStyle(refBBFS.current).getPropertyValue('font-size');
    console.log('my app effect', BaseWidthPx, BaseFSPx);
  };

  getBaseWidth();

  // resize事件处理逻辑保持不变
  function HandleResolutionChange() {
    if (!refBBFS.current) return;
    console.log('** executing resolution change ** ', 
      window.getComputedStyle(refBBFS.current).getPropertyValue('width'),
      window.getComputedStyle(refBBFS.current).getPropertyValue('font-size')
    );
  }
  window.addEventListener('resize', HandleResolutionChange);

  return () => {
    window.removeEventListener('resize', HandleResolutionChange);
  };
}, []);

2. 用requestAnimationFrame延迟计算

让计算逻辑延迟到浏览器下一次重绘前执行,确保布局已完全渲染:

useEffect(() => {
  const getBaseWidth = () => {
    // 延迟到下一次浏览器重绘时执行
    requestAnimationFrame(() => {
      if (!refBBFS.current) return;
      
      const BaseWidthPx = window.getComputedStyle(refBBFS.current).getPropertyValue('width');
      const BaseFSPx = window.getComputedStyle(refBBFS.current).getPropertyValue('font-size');
      console.log('my app effect', BaseWidthPx, BaseFSPx);
    });
  };

  getBaseWidth();

  // resize事件处理逻辑保持不变
  function HandleResolutionChange() {
    if (!refBBFS.current) return;
    console.log('** executing resolution change ** ', 
      window.getComputedStyle(refBBFS.current).getPropertyValue('width'),
      window.getComputedStyle(refBBFS.current).getPropertyValue('font-size')
    );
  }
  window.addEventListener('resize', HandleResolutionChange);

  return () => {
    window.removeEventListener('resize', HandleResolutionChange);
  };
}, []);

3. 监听自定义字体加载事件

如果使用的是自定义字体,可以直接监听字体的加载完成事件:

useEffect(() => {
  // 初始化自定义字体
  const customFont = new FontFace('some monospace font', 'url(/path/to/your-font-file.ttf)');
  
  const getBaseWidth = () => {
    if (!refBBFS.current) return;
    const BaseWidthPx = window.getComputedStyle(refBBFS.current).getPropertyValue('width');
    const BaseFSPx = window.getComputedStyle(refBBFS.current).getPropertyValue('font-size');
    console.log('my app effect', BaseWidthPx, BaseFSPx);
  };

  // 加载字体并添加到文档后再计算
  customFont.load().then(() => {
    document.fonts.add(customFont);
    getBaseWidth();
  });

  // resize事件处理逻辑保持不变
  function HandleResolutionChange() {
    if (!refBBFS.current) return;
    console.log('** executing resolution change ** ', 
      window.getComputedStyle(refBBFS.current).getPropertyValue('width'),
      window.getComputedStyle(refBBFS.current).getPropertyValue('font-size')
    );
  }
  window.addEventListener('resize', HandleResolutionChange);

  return () => {
    window.removeEventListener('resize', HandleResolutionChange);
  };
}, []);

4. Next.js禁用SSR动态导入组件

如果是服务器端渲染导致的布局差异,可以将计算宽度的组件动态导入并禁用SSR,确保在客户端完全渲染后执行计算:

// 页面组件中
import dynamic from 'next/dynamic';

// 动态导入计算组件,禁用SSR
const WidthCalculator = dynamic(() => import('./WidthCalculator'), { ssr: false });

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      <WidthCalculator />
      <Component {...pageProps} />
    </>
  );
}

然后在WidthCalculator组件中实现原有的ref和计算逻辑即可。

内容的提问来源于stack exchange,提问作者omer brandis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 01:52:10