React JS:使用object-fit: cover CSS属性,重新加载时图像大小闪烁

5

我在使用CRA创建的React应用程序中显示了一张图片。每次重新加载网页时,图片会出现奇怪的闪烁如下图所示。

图片初始状态:

输入图像描述

闪烁后的图片(实际要求):

输入图像描述

代码:

styles.css:

img {
  width: 200px;
  height: 300px;
  object-fit: cover;
}

app.tsx

import React from 'react';
import foo from './foo.jpg';

import './styles.css';

const App = () => <img src={foo} alt="foo" />;

export default App;

package.json

{
  "name": "xyz",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

注:

  1. 该应用程序是一个注入的样板,没有安装任何第三方库或除此图像之外的任何其他组件。
  2. 如果从样式表中删除object-fit:cover;属性,则上述问题会得到解决,但这是为了防止图片像初始图片一样被奇怪地拉伸/ 收缩而需要的。
  3. 如果无法重现此问题,请保持开发人员控制台打开或尝试将网络更改为任何 3G 预设。 我可以轻松地通过反复重新加载来重现。
  4. 我认为object-fit: cover属性最初不适用于图像,并需要几毫秒才能启动。
  5. 请注意,在初始图片中,DOM 结构不显示在开发人员控制台中。
  6. 任何替代 CSS 属性也将非常有帮助。

@DennisVash 在 codesandbox 上无法重现。 - Vinay Sharma
@DennisVash 我认为codesandbox并没有在实际浏览器中呈现React应用程序,我们所看到的只是React在他们平台上呈现在一个div中。因此,如果我重新加载codesandbox,它不会像在系统上的实际浏览器那样工作。如果我尝试通过单击提供的重新加载图标来重新加载,情况也是如此。 - Vinay Sharma
这不是它的工作方式... 你实际上说你在浏览器中运行一个浏览器,codesandbox只是渲染你编写的代码,你可以检查它并自己看到,此外它有CRA starter,因此它完全模拟任何机器。 - Dennis Vash
奇怪的是,我确实可以在Chromium浏览器上复现这个问题,只需要使用标准的HTML/CSS,不需要React。正如你所说,在在线编辑器中无法复制此问题。更奇怪的是,我在网上找不到任何相关信息。 - lawrence-witt
如果您处于开发模式下,CSS 更新可能会很慢。尝试构建它以查看在生产环境中它的外观,其中 CSS 已经捆绑到头部。 - Steve Tomlin
显示剩余5条评论
1个回答

11

经过一段时间的试验,我相信这是Chromium浏览器中的性能问题。具体而言,它似乎与图像缓存和浏览器在将图像加载到带有显式高度和宽度尺寸的img容器时所做的优化有关。

我之所以这么说,是因为在我的测试中,在重新加载页面之前清空缓存会使浏览器在第一次绘制之前应用object-fit。我还注意到,无论是否存在object-fit,缓存中的图像都将在比只有一个或没有这些尺寸的img容器更快地加载到具有定义高度和宽度的img容器中。这似乎是发生错误的地方。第二层绘画层才会应用object-fit规则。

也许您可以使用以下代码片段尝试重现此问题以进行确认?我在此处使用了随机的占位符图片,但您可能想使用本地图片。如评论中所述,这可通过仅使用HTML/CSS和网络限制来重现,尽管我发现在React上下文中效果更加明显:

const { Fragment } = React;

const App = () => {
  return (
    <Fragment>
    
      <p>Object fit with two explicit dimensions</p>
      <img src="https://source.unsplash.com/RN6ts8IZ4_0/600x400" className="img-fit"></img>
      
      <p>Positioned element with one explicit dimension</p>
      <div className="container">
        <img src="https://source.unsplash.com/RN6ts8IZ4_0/600x400" className="img-position"></img>
      </div>
      
    </Fragment>
  );
};

ReactDOM.render(<App/>, document.body);
.img-fit {
  width: 200px;
  height: 300px;
  object-fit: cover;
}

.container {
  width: 200px;
  height: 300px;
  overflow: hidden;
  position: relative;
}

.img-position {
  height: 100%;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

无论如何,如果你仍然遇到麻烦,以上代码段中的第二张图片是一种可能的解决方法。通过使img高度或宽度为100%,您可以在父容器上选择要缩放的尺寸。然后,结合方向偏移和变换,可以将其居中(或进行其他调整)。这显然不像object-fit那样动态,因为您必须事先决定如何缩放图像以获得covercontain效果。
一些替代解决方案包括: 1. 使用背景图像 我发现最终源于object-fit的CSS属性background不会出现同样的错误。使用div而不是img标记,并将图像放在其背景上:
div {
  width: 200px;
  height: 300px;
  background: url('path/yourimg.jpg') center / cover;
}

2. 预加载(HTML)

我发现的另一个解决方法是预加载图像,这会向浏览器发出信号,表明这是关键资源,应立即获取。这似乎支持了问题最终归结为浏览器优化的想法。

不幸的是,这并不完全符合 React 的标准,因为您只有一个 HTML 页面,它很容易被这些链接填满,但无论如何都可以使用。将其放入 head 中:

<link rel="preload" href="path/yourimage.jpg" as="image"/>

3. 预加载 (React)

在React中,您可以通过等待图像的 onload 事件触发后再将其显示给用户来模拟预加载。这可以使用JSX中带有相关类的一行代码实现:

// app.js

const App = () => <img src={foo} onLoad={e => e.target.classList.add('visible')}/>;

// index.css

img {
  width: 200px;
  height: 300px;
  object-fit: cover;
  visibility: hidden;
}

.visible {
  visibility: visible;
};

或者,如果您正在加载一批图像,您可以在显示组件/批次本身之前等待它们全部加载完成。这种方法存在潜在的布局移位问题,并且可能需要一些优化处理大批量的情况,但您已经有了想法:

import foo from 'path/foo.jpg';
import bar from 'path/bar.jpg';

const App = () => {
    const [loaded, setLoaded] = useState(false);

    useEffect(() => {
        const loadArray = [foo, bar].map(src => {
            return new Promise((res, rej) => {
                const img = new Image();

                img.src = src;
                img.onload = () => res();
                img.onerror = () => rej();
            })
        });

        Promise.allSettled(loadArray).then(() => setLoaded(true));
    }, []);

    return loaded && (
        <>
            <img src={foo} alt="foo"></img>
            <img src={bar} alt="bar"></img>
        </>
    );
};

我非常感谢您在这个问题上深入挖掘。我会仔细阅读您的答案。但是我已经简要阅读了一下,并且想补充一点,即图像不是嵌入在应用程序中,而是存储在服务器上。因此,我认为使用<link>不是一个好的做法,而且我将不得不为我的应用程序中的所有组件添加内联样式,以便为每个组件提供背景图像,这将消耗性能。另外,有没有办法针对Chromium浏览器或特定的Chrome生产版本提出问题? - Vinay Sharma
你的这篇文章真是太棒了,你是个传奇!我简直不敢相信这篇帖子竟然这么冷清。它就像一块黄金,非常感谢你! - Jimmy
使用背景图像有效!谢谢!我的用例:<div style={{ background: \url('${imageUrl}') center / contain`, backgroundRepeat: 'no-repeat' }}></div>` - Максим Мартынов

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接