Shopify / polaris

Shopify’s design system to help us work together to build a great experience for all of our merchants.
https://polaris.shopify.com
Other
5.77k stars 1.17k forks source link

Polaris tokens optimization - reduce bundle size #11808

Open juzser opened 5 months ago

juzser commented 5 months ago

Summary

Hi there, I've created a discussion topic here. I see the polaris-tokens package size is large, I investigate and see it contains many non-tokenized or variables data, that increase the polaris-tokens package size a bit.

If Polaris team can take a look, we can reduce the bundle size.

Thank you.

Rationale

daviareias commented 1 month ago

I was able to shave around 145kb from the initial load of javascript (around 15kb gziped) by removing most of the unused code by using this instead of polaris-tokens:

import { taggedTemplateLiteralLoose, slicedToArray } from "./_rollupPluginBabelHelpers";
const themes = {
    light: {
        breakpoints: {
            "breakpoints-xs": "0rem",
            "breakpoints-sm": "30.625rem",
            "breakpoints-md": "48rem",
            "breakpoints-lg": "65rem",
            "breakpoints-xl": "90rem",
        },
        space: {
            "space-500": "1.25rem",
        },
        motion: {
            "motion-duration-100": "100ms",
        },
    },
    "light-mobile": {},
    "light-high-contrast-experimental": {},
    "dark-experimental": {},
};

let _templateObject;
const BASE_FONT_SIZE = 16;
const UNIT_PX = "px";
const UNIT_EM = "em";
const UNIT_REM = "rem";

function toPx(value?: string) {
    if (value === void 0) {
        value = "";
    }
    const unit = getUnit(value);
    if (!unit) return value;
    if (unit === UNIT_PX) {
        return value;
    }
    if (unit === UNIT_EM || unit === UNIT_REM) {
        return "" + parseFloat(value) * BASE_FONT_SIZE + UNIT_PX;
    }
}

function createThemeClassName(themeName: string) {
    return "p-theme-" + themeName;
}

function getUpMediaCondition(breakpoint: string) {
    return "(min-width: " + toEm(breakpoint) + ")";
}

function getDownMediaCondition(breakpoint: string) {
    let _toPx2;
    const offsetBreakpoint = parseFloat((_toPx2 = toPx(breakpoint)) != null ? _toPx2 : "") - 0.04;
    return "(max-width: " + toEm(offsetBreakpoint + "px") + ")";
}

function toEm(value?: string, fontSize?: number) {
    if (value === void 0) {
        value = "";
    }
    if (fontSize === void 0) {
        fontSize = BASE_FONT_SIZE;
    }
    const unit = getUnit(value);
    if (!unit) return value;
    if (unit === UNIT_EM) {
        return value;
    }
    if (unit === UNIT_PX) {
        return "" + parseFloat(value) / fontSize + UNIT_EM;
    }
    if (unit === UNIT_REM) {
        return "" + (parseFloat(value) * BASE_FONT_SIZE) / fontSize + UNIT_EM;
    }
}

// https://regex101.com/r/zvY2bu/1
const DIGIT_REGEX = new RegExp(String.raw(_templateObject || (_templateObject = taggedTemplateLiteralLoose(["-?d+(?:.d+|d*)"], ["-?\\d+(?:\\.\\d+|\\d*)"]))));
const UNIT_REGEX = new RegExp(UNIT_PX + "|" + UNIT_EM + "|" + UNIT_REM);

function getUnit(value?: string) {
    if (value === void 0) {
        value = "";
    }
    const unit = value.match(new RegExp(DIGIT_REGEX.source + "(" + UNIT_REGEX.source + ")"));
    return unit && unit[1];
}
function getMediaConditions(breakpoints: string[]) {
    const breakpointEntries = Object.entries(breakpoints);
    const lastBreakpointIndex = breakpointEntries.length - 1;
    return Object.fromEntries(
        breakpointEntries.map(function (entry, index) {
            const _ref3 = entry,
                _ref4 = slicedToArray(_ref3, 2),
                breakpointsTokenName = _ref4[0],
                breakpoint = _ref4[1];
            const upMediaCondition = getUpMediaCondition(breakpoint);
            const downMediaCondition = getDownMediaCondition(breakpoint);
            const onlyMediaCondition =
                index === lastBreakpointIndex ? upMediaCondition : upMediaCondition + " and " + getDownMediaCondition(breakpointEntries[index + 1][1]);
            return [
                breakpointsTokenName,
                {
                    // Media condition for the current breakpoint and up
                    up: upMediaCondition,
                    // Media condition for current breakpoint and down
                    down: downMediaCondition,
                    // Media condition for only the current breakpoint
                    only: onlyMediaCondition,
                },
            ];
        }),
    );
}

const themeNames = ["light", "light-mobile", "light-high-contrast-experimental", "dark-experimental"],
    breakpointsAliases = ["xs", "sm", "md", "lg", "xl"],
    themeNameDefault = "light",
    /** this has to return themes["light"] */
    themeDefault = themes["light"];

export { themeNameDefault, toPx, createThemeClassName, breakpointsAliases, getMediaConditions, themes, themeDefault, themeNames };
juzser commented 1 month ago

Sound goods! Thank you. But have you tested it? Is it missing anything? I agree that polaris-tokens has a lot of unused variables, but sometimes, you want to change between the variants of a component, they will be necessary.

@alex-page does anyone can verify this please? Thank you 🙏🏽

daviareias commented 1 month ago

Sound goods! Thank you. But have you tested it? Is it missing anything? I agree that polaris-tokens has a lot of unused variables, but sometimes, you want to change between the variants of a component, they will be necessary.

@alex-page does anyone can verify this please? Thank you 🙏🏽

I went file by file and it seems that polaris-tokens is mostly used by polaris to check the style according to the device screen size. The only exception is "ThemeProvider" which is a really old component that was deprecated way back, see https://github.com/Shopify/polaris/pull/6334.

I didn't include the code for this part: import { taggedTemplateLiteralLoose, slicedToArray } from "./_rollupPluginBabelHelpers";

But this is also just a file that crates the regex that will check if the unit is in px or em. This part may also not be necessary, but here is the code for the two exported functions:

function _iterableToArrayLimit(arr, i) {
    var _i = null == arr ? null : ("undefined" != typeof Symbol && arr[Symbol.iterator]) || arr["@@iterator"];
    if (null != _i) {
        var _s,
            _e,
            _x,
            _r,
            _arr = [],
            _n = !0,
            _d = !1;
        try {
            if (((_x = (_i = _i.call(arr)).next), 0 === i)) {
                if (Object(_i) !== _i) return;
                _n = !1;
            } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0);
        } catch (err) {
            (_d = !0), (_e = err);
        } finally {
            try {
                if (!_n && null != _i.return && ((_r = _i.return()), Object(_r) !== _r)) return;
            } finally {
                if (_d) throw _e;
            }
        }
        return _arr;
    }
}
function _taggedTemplateLiteralLoose(strings, raw) {
    if (!raw) {
        raw = strings.slice(0);
    }
    strings.raw = raw;
    return strings;
}
function _slicedToArray(arr, i) {
    return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _arrayWithHoles(arr) {
    if (Array.isArray(arr)) return arr;
}
function _unsupportedIterableToArray(o, minLen) {
    if (!o) return;
    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
    var n = Object.prototype.toString.call(o).slice(8, -1);
    if (n === "Object" && o.constructor) n = o.constructor.name;
    if (n === "Map" || n === "Set") return Array.from(o);
    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
    if (len == null || len > arr.length) len = arr.length;
    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
    return arr2;
}
function _nonIterableRest() {
    throw new TypeError("Invalid attempt");
}

export { _slicedToArray as slicedToArray, _taggedTemplateLiteralLoose as taggedTemplateLiteralLoose };
daviareias commented 1 month ago

The way that I managed to replace the library in vite is by adding this to the config:

/** @see https://vitejs.dev/config/ */
export default defineConfig(({ command, mode }) => {
    return {
        // rest of config
        resolve: {
            alias: {
                // put your code in my-polaris-tokens or your favorite folder
                "@shopify/polaris-tokens": path.resolve(__dirname, "./my-polaris-tokens"),
            },
        },
    };
});

If you're not using vite, you have to check in your bundler how to achieve the same.