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
相关产品推荐
相关产品推荐

