Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use exponential increase for srcset widths #224

Merged
merged 7 commits into from
Nov 26, 2018
3 changes: 1 addition & 2 deletions src/react-imgix.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ function buildSrc({
const url = constructUrl(rawSrc, urlParams);
return `${url} ${targetWidth}w`;
};
const addFallbackSrc = srcSet => srcSet.concat(src);
srcSet = addFallbackSrc(targetWidths.map(buildSrcSetPair)).join(", ");
srcSet = targetWidths.map(buildSrcSetPair).join(", ");

if (showARWarning && config.warnings.invalidARFormat) {
console.warn(
Expand Down
106 changes: 11 additions & 95 deletions src/targetWidths.js
Original file line number Diff line number Diff line change
@@ -1,102 +1,18 @@
// Taken from https://github.com/imgix/imgix.js/blob/eff194416a0efc9f0c528eb070466cd474ae0bf5/src/targetWidths.js

function uniq(arr) {
var n = {},
r = [],
i;

for (i = 0; i < arr.length; i++) {
if (!n[arr[i]]) {
n[arr[i]] = true;
r.push(arr[i]);
}
}

return r;
}

const MINIMUM_SCREEN_WIDTH = 100;
const MAXIMUM_SCREEN_WIDTH = 2560;
const MAX_DPR = 2;
const SCREEN_STEP = 100;

// Screen data from http://gs.statcounter.com/screen-resolution-stats/mobile/worldwide

// With these resolutions we cover 80% of mobile devices
// prettier-ignore
const MOBILE_RESOLUTIONS = [320, 360, 375, 414, 480, 540, 640, 720, 1080]

// With these resolutions we cover 90% of tablets
// prettier-ignore
const TABLET_RESOLUTIONS = [600, 768, 800, 962, 1024, 1280];

// With these resolutions we cover 80% of desktops
// prettier-ignore
const DESKTOP_RESOLUTIONS = [1024, 1280, 1366, 1440, 1536, 1600, 1680, 1820, 1920]

// Bootstrap breakpoints
const BOOTSTRAP_SM = { cssWidth: 576, dpr: 1 };
const BOOTSTRAP_SM_2X = { cssWidth: 576, dpr: 2 };
const BOOTSTRAP_MD = { cssWidth: 720, dpr: 1 };
const BOOTSTRAP_MD_2X = { cssWidth: 720, dpr: 2 };
const BOOTSTRAP_LG = { cssWidth: 940, dpr: 1 };
const BOOTSTRAP_LG_2X = { cssWidth: 940, dpr: 2 };
const BOOTSTRAP_XL = { cssWidth: 1140, dpr: 1 };
const BOOTSTRAP_XL_2X = { cssWidth: 1140, dpr: 2 };

const BOOTSTRAP_BREAKS = [
BOOTSTRAP_SM,
BOOTSTRAP_SM_2X,
BOOTSTRAP_MD,
BOOTSTRAP_MD_2X,
BOOTSTRAP_LG,
BOOTSTRAP_LG_2X,
BOOTSTRAP_XL,
BOOTSTRAP_XL_2X
];

function deviceWidths() {
const widths = [
...MOBILE_RESOLUTIONS,
...TABLET_RESOLUTIONS,
...DESKTOP_RESOLUTIONS,
...BOOTSTRAP_BREAKS.map(device => device.cssWidth * device.dpr)
];
return widths;
}
function targetWidths() {
const resolutions = [];
let prev = 100;
const INCREMENT_PERCENTAGE = 8;
const MAX_SIZE = 8192;

// Generates an array of physical screen widths to represent
// the different potential viewport sizes.
//
// We step by `SCREEN_STEP` to give some sanity to the amount
// of widths we output.
//
// The upper bound is the widest known screen on the planet.
// @return {Array} An array of {Fixnum} instances
function screenWidths(maxWidth) {
const widths = [];
const ensureEven = n => 2 * Math.round(n / 2);

for (let i = MINIMUM_SCREEN_WIDTH; i < maxWidth; i += SCREEN_STEP) {
widths.push(i);
resolutions.push(prev);
while (prev <= MAX_SIZE) {
prev *= 1 + (INCREMENT_PERCENTAGE / 100) * 2;
resolutions.push(ensureEven(prev));
}
widths.push(maxWidth);

return widths;
}

// Return the widths to generate given the input `sizes`
// attribute.
//
// @return {Array} An array of {Fixnum} instances representing the unique `srcset` URLs to generate.
function targetWidths() {
const minPxWidthRequired = SCREEN_STEP;
const maxPxWidthRequired = MAXIMUM_SCREEN_WIDTH * MAX_DPR;
const _screenWidths = screenWidths(maxPxWidthRequired);
const allWidths = deviceWidths().concat(_screenWidths);

return uniq(allWidths).sort(function(x, y) {
return x - y;
});
return resolutions;
}

export default targetWidths();
66 changes: 57 additions & 9 deletions test/unit/react-imgix.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ReactDOM from "react-dom";
import { shallow as enzymeShallow, mount } from "enzyme";
import PropTypes from "prop-types";
import { shallowUntilTarget } from "../helpers";
import targetWidths from "targetWidths";

import Imgix, {
__ReactImgixImpl,
Expand Down Expand Up @@ -71,15 +72,62 @@ describe("When in default mode", () => {
const sut = shallow(<Imgix src={src} sizes="100vw" />);
expect(sut.type()).toBe("img");
});
it("the rendered element should have a srcSet set correctly", async () => {
const sut = shallow(<Imgix src={src} sizes="100vw" />);
const srcSet = sut.props().srcSet;
expect(srcSet).not.toBeUndefined();
expect(srcSet.split(", ")[0].split(" ")).toHaveLength(2);
const aSrcFromSrcSet = srcSet.split(", ")[0].split(" ")[0];
expect(aSrcFromSrcSet).toContain(src);
const aWidthFromSrcSet = srcSet.split(", ")[0].split(" ")[1];
expect(aWidthFromSrcSet).toMatch(/^\d+w$/);
describe("srcset", () => {
it("the rendered element should have a srcSet set correctly", async () => {
const sut = shallow(<Imgix src={src} sizes="100vw" />);
const srcset = sut.props().srcSet;
expect(srcset).not.toBeUndefined();
expect(srcset.split(", ")[0].split(" ")).toHaveLength(2);
const aSrcFromSrcSet = srcset.split(", ")[0].split(" ")[0];
expect(aSrcFromSrcSet).toContain(src);
const aWidthFromSrcSet = srcset.split(", ")[0].split(" ")[1];
expect(aWidthFromSrcSet).toMatch(/^\d+w$/);
});
it("returns the expected number of `url widthDescriptor` pairs", function() {
const sut = shallow(<Imgix src={src} sizes="100vw" />);
const srcset = sut.props().srcSet;

expect(srcset.split(",").length).toEqual(targetWidths.length);
});

it("should not exceed the bounds of [100, 8192]", () => {
const sut = shallow(<Imgix src={src} sizes="100vw" />);
const srcset = sut.props().srcSet;

const srcsetWidths = srcset
.split(", ")
.map(srcset => srcset.split(" ")[1])
.map(width => width.slice(0, -1))
.map(Number.parseFloat);

const min = Math.min(...srcsetWidths);
const max = Math.max(...srcsetWidths);

expect(min).not.toBeLessThan(100);
expect(min).not.toBeGreaterThan(8192);
});

// 18% used to allow +-1% for rounding
it("should not increase more than 18% every iteration", () => {
const INCREMENT_ALLOWED = 0.18;

const sut = shallow(<Imgix src={src} sizes="100vw" />);
const srcset = sut.props().srcSet;

const srcsetWidths = srcset
.split(", ")
.map(srcset => srcset.split(" ")[1])
.map(width => width.slice(0, -1))
.map(Number.parseFloat);

let prev = srcsetWidths[0];

for (let index = 1; index < srcsetWidths.length; index++) {
const element = srcsetWidths[index];
expect(element / prev).toBeLessThan(1 + INCREMENT_ALLOWED);
prev = element;
}
});
});
});

Expand Down