Skip to content

Commit

Permalink
feat: add performance warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
atcastle authored and luqven committed Oct 15, 2024
1 parent f316b53 commit ba1b1d5
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ The warnings available are:
| fallbackImage | Triggered when there is no `<img>` or `<Imgix>` at the end of the children when using `<Picture>`. A fallback image is crucial to ensure the image renders correctly when the browser cannot match against the sources provided |
| sizesAttribute | This library requires a `sizes` prop to be passed so that the images can render responsively. This should only turned off in very special circumstances. |
| invalidARFormat | Warnings thrown when the `ar` imgix parameter is not passed in the correct format (`w:h`) |
| oversizeImage | A runtime error triggered when an image loads with an intrinsic size substantially larger than the rendered size. |
| lazyLCP | A runtime error triggered when an image is detected to be the [LCP element](https://web.dev/articles/lcp) but is loaded with `loading="lazy"`. |

## Upgrade Guides

Expand Down
2 changes: 2 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const config = {
fallbackImage: true,
sizesAttribute: true,
invalidARFormat: true,
oversizeImage: true,
lazyLCP: true
},
};

Expand Down
76 changes: 76 additions & 0 deletions src/react-imgix.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const REACT_IMGIX_PROP_TYPES = Object.assign(
}
);

const OVERSIZE_IMAGE_TOLERANCE = 500;

let performanceObserver;

/**
* Validates that an aspect ratio is in the format w:h. If false is returned, the aspect ratio is in the wrong format.
*/
Expand Down Expand Up @@ -188,6 +192,75 @@ function buildSrc({
};
}

/**
* Use the PerfomanceObser API to warn if an LCP element is loaded lazily.
*/
function watchForLazyLCP(imgRef) {
if (
!performanceObserver &&
typeof window !== 'undefined' &&
window.PerformanceObserver
) {
performanceObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();

if (entries.length === 0) {
return;
}

// The most recent LCP entry is the only one that can be the real LCP element.
const lcpCandidate = entries[entries.length - 1];
if (lcpCandidate.element?.getAttribute("loading") === "lazy") {
console.warn(
`An image with URL ${imgRef.src} was detected as a possible LCP element (https://web.dev/lcp) ` +
`and also has 'loading="lazy"'. This can have a significant negative impact on page loading performance. ` +
`Lazy loading is not recommended for images which may render in the initial viewport.` );
}
});
performanceObserver.observe({type: 'largest-contentful-paint', buffered: true});
}
}

/**
* Once the image is loaded, warn if it's intrinsic size is much larger than its rendered size.
*/
function checkImageSize(imgRef) {
const renderedWidth = imgRef.clientWidth;
const renderedHeight = imgRef.clientHeight;
const intrinsicWidth = imgRef.naturalWidth;
const intrinsicHeight = imgRef.naturalHeight;

if (
intrinsicWidth > renderedWidth + OVERSIZE_IMAGE_TOLERANCE ||
intrinsicHeight > renderedHeight + OVERSIZE_IMAGE_TOLERANCE
) {
console.warn(
`An image with URL ${imgRef.src} was rendered with dimensions significantly smaller than intrinsic size, ` +
`which can slow down page loading. This may be caused by a missing or inaccurate "sizes" property. ` +
`Rendered size: ${renderedWidth}x${renderedHeight}. Intrinsic size: ${intrinsicWidth}x${intrinsicHeight}.`
);
}
}

/**
* Initializes listeners for performance-related image warnings
*/
function doPerformanceChecksOnLoad(imgRef) {
// Check image size on load
if(config.warnings.oversizeImage) {
if (imgRef.complete) {
checkImageSize(imgRef);
} else {
imgRef.addEventListener('load', () => {
checkImageSize(imgRef);
});
}
}
if(config.warnings.lazyLCP) {
watchForLazyLCP(imgRef);
}
}

/**
* Combines default imgix params with custom imgix params to make a imgix params config object
*/
Expand All @@ -212,6 +285,9 @@ class ReactImgix extends Component {
}

componentDidMount() {
if (NODE_ENV === 'development' && this.imgRef) {
doPerformanceChecksOnLoad(this.imgRef);
}
this.props.onMounted(this.imgRef);
}

Expand Down
87 changes: 87 additions & 0 deletions test/integration/react-imgix.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,93 @@ const renderAndWaitForImageLoad = async (element) => {
});
};

describe("Image Warnings", () => {
const imageUndersizedWarning = "was rendered with dimensions significantly smaller than intrinsic size";
const lcpWarning = "was detected as a possible LCP element";

let realConsoleWarn;
let warnings = [];

beforeEach(() => {
realConsoleWarn = window.console.warn;
window.console.warn = (str) => {
warnings.push(str);
}
});
afterEach(() => {
window.console.warn = realConsoleWarn;
warnings = [];
});

it("should log a warning if LCP image is lazy-loaded", async () => {
const renderedImage = await renderAndWaitForImageLoad(
<Imgix
src={src}
sizes="100vw"
htmlAttributes={{
loading: "lazy"
}}
/>
);

await new Promise((resolve) => {
setTimeout(resolve, 500);
});
expect(warnings.find(warning => warning.includes(lcpWarning))).toBeTruthy();
});

it("should not log a warning if non-LCP image is lazy-loaded", async () => {
renderIntoContainer(
<div>
<div style={{height: "5000px", width: "5000px"}}></div>
<Imgix
src={src}
sizes="100vw"
htmlAttributes={{
loading: "lazy"
}}
/>
</div>
);

await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
expect(warnings.find(warning => warning.includes(lcpWarning))).not.toBeTruthy();
});

it("should log a warning if intrinsic dimensions are significantly larger than rendered size", async () => {

const sut = await renderAndWaitForImageLoad(
<Imgix
src={src}
sizes="100w"
htmlAttributes={{
style: { width: '400px', height: '400px' }
}}
/>
);

expect(warnings.find(warning => warning.includes(imageUndersizedWarning))).toBeTruthy();
});

it("should not log a warning if intrinsic dimensions are within the threshold of rendered size", async () => {

const sut = await renderAndWaitForImageLoad(
<Imgix
src={src}
sizes="500px"
htmlAttributes={{
style: { width: '400px', height: '400px' }
}}
/>
);

expect(warnings.find(warning => warning.includes(imageUndersizedWarning))).not.toBeTruthy();
});

});

describe("When in default mode", () => {
const renderImage = () =>
renderIntoContainer(<Imgix src={src} sizes="100px" />);
Expand Down

0 comments on commit ba1b1d5

Please sign in to comment.