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

perf: prioritize the main bundle over <link rel=preload>s #23556

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions app/client/craco.build.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,37 @@ module.exports = merge(common, {
},
},
},
// Emit the main `script` without a `defer` attribute. This increases its priority
// from Low to High (doc: https://addyosmani.com/blog/script-priorities/) and prevents it
// from competing with `<link rel="preload"`>s in `index.html`, which are also Low.
{
plugin: {
overrideWebpackConfig: ({ webpackConfig }) => {
const htmlWebpackPlugin = webpackConfig.plugins.find(
(plugin) => plugin.constructor.name === "HtmlWebpackPlugin",
);

// CRA must include HtmlWebpackPlugin: https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/react-scripts/config/webpack.config.js#L608-L632
// If it doesn’t, perhaps the version of CRA has changed, or plugin names got mangled?
assert(
htmlWebpackPlugin,
"Cannot find HtmlWebpackPlugin in webpack config",
);

// HtmlWebpackPlugin must have the userOptions field: https://github.com/jantimon/html-webpack-plugin/blob/d5ce5a8f2d12a2450a65ec51c285dd54e36cd921/index.js#L34.
// If it doesn’t, perhaps the version of HtmlWebpackPlugin has changed?
assert(
htmlWebpackPlugin.userOptions,
"htmlWebpackPlugin.userOptions must be defined",
);

htmlWebpackPlugin.userOptions.inject = "head";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamakulov , What happens if we put this main at the end of body? Does it have to compete for bandwidth with preloads?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this request was scheduled late-ish in my tests (due to being discovered later), and one or two <link rel="preload"> stole its bandwidth. I’ll re-run the tests and get back with waterfalls in a bit!

Copy link
Contributor Author

@iamakulov iamakulov Jun 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, re-ran the tests. With <script> in the end of <head> (as in this PR), the main bundle takes 2.1s (request 5):

waterfall (21)

HTML code
<!doctype html>
<html lang="en">

<head>
    <script>navigator.serviceWorker.register = () => { };</script>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
    <title>Appsmith</title>
    <style>
        #loader {
            position: fixed;
            left: 0;
            top: 0;
            height: 4px;
            background: #d7d7d7;
            transition: all ease-in .3s
        }
    </style>
    <link rel="preload" as="script" href="/static/js/2209.1a60b0b6.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/55178.ef82f381.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/81621.58d65afe.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/89532.5ad27563.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/85342.fd2d6599.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/3638.68e4f960.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/51863.ac47ac64.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/global-search.3bd25387.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/82492.1f73e40c.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/66124.a789a752.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/7207.9189ae78.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/29020.74dfd06a.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/72527.ce5b870f.chunk.js" fetchpriority="low" />
    <link rel="preload" as="style" href="/static/css/editor.6f95ab40.chunk.css" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/editor.8486711d.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/2209.1a60b0b6.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/55178.ef82f381.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/51863.ac47ac64.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/global-search.3bd25387.chunk.js" fetchpriority="low" />
    <script>const parseConfig = e => { if (0 === e.indexOf("__") || 0 === e.indexOf("$") || 0 === e.indexOf("%")) return ""; const o = e.trim(); return "false" !== o.toLowerCase() && "" !== o && ("true" === o.toLowerCase() || o) }, CLOUD_HOSTING = parseConfig("true"), ZIPY_KEY = parseConfig(""), AIRGAPPED = parseConfig("false"); if (CLOUD_HOSTING && ZIPY_KEY && !AIRGAPPED) { const e = document.createElement("script"); e.crossOrigin = "anonymous", e.defer = !0, e.src = "https://cdn.zipy.ai/sdk/v1.0/zipy.min.umd.js", e.onload = () => { window.zipy && window.zipy.init(ZIPY_KEY) }; const o = document.getElementsByTagName("head")[0]; o && o.appendChild(e) } gapiLoaded = () => { window.googleAPIsLoaded = !0 }, onError = () => { window.googleAPIsLoaded = !1 }</script>
    <script async defer="defer" id="googleapis" src="https://apis.google.com/js/api.js" onload="gapiLoaded()"
        onerror="onError()"></script>
    <script src="/static/js/main.c65af675.js"></script>
    <link href="/static/css/main.c3722dd4.css" rel="stylesheet">
</head>

<body class="appsmith-light-theme"><noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="loader" style="width:30vw"></div>
    <div id="header-root"></div>
    <div id="root"></div>
    <script
        type="text/javascript">const getIsLocalStorageSupported = () => { try { return window.localStorage.setItem("test", "testA"), window.localStorage.removeItem("test"), !0 } catch (e) { return !1 } }, isLocalStorageSupported = getIsLocalStorageSupported(), handleLocalStorageNotSupportedError = () => { console.error("Localstorage storage is not supported on your device.") }, localStorageUtil = { getItem: e => { if (isLocalStorageSupported) return window.localStorage.getItem(e); handleLocalStorageNotSupportedError() }, removeItem: e => { if (isLocalStorageSupported) return window.localStorage.removeItem(e); handleLocalStorageNotSupportedError() }, setItem: (e, r) => { if (isLocalStorageSupported) return window.localStorage.setItem(e, r); handleLocalStorageNotSupportedError() } }; window.addEventListener("DOMContentLoaded", (e => { document.getElementById("loader").style.width = "50vw" })); const registerPageServiceWorker = () => { "serviceWorker" in navigator && !window.Cypress && window.addEventListener("load", (function () { navigator.serviceWorker.register("/pageService.js").catch((e => { console.error("Service Worker Registration failed: " + e) })) })) }; "serviceWorker" in navigator && !window.Cypress && window.addEventListener("load", (function () { navigator.serviceWorker.register("/pageService.js").catch((e => { console.error("Service Worker Registration failed: " + e) })) }))</script>
    <script
        type="text/javascript">const LOG_LEVELS = ["debug", "error"], CONFIG_LOG_LEVEL_INDEX = LOG_LEVELS.indexOf(parseConfig("")), INTERCOM_APP_ID = parseConfig("%REACT_APP_INTERCOM_APP_ID%") || parseConfig("y10e7138"), DISABLE_INTERCOM = parseConfig("") || parseConfig("false"); INTERCOM_APP_ID.length && !DISABLE_INTERCOM && function () { var _ = window, e = _.Intercom; if ("function" == typeof e) e("reattach_activator"), e("update", _.intercomSettings); else { var E = document, a = function () { a.c(arguments) }; a.q = [], a.c = function (_) { a.q.push(_) }, _.Intercom = a; var I = function () { var _ = E.createElement("script"); _.type = "text/javascript", _.async = !0, _.src = "https://widget.intercom.io/widget/" + INTERCOM_APP_ID; var e = E.getElementsByTagName("script")[0]; e.parentNode.insertBefore(_, e) }; "complete" === document.readyState ? I() : _.attachEvent ? _.attachEvent("onload", I) : _.addEventListener("load", I, !1) } }(), window.SENTRY_CONFIG = parseConfig("https://abf15a075d1347969df44c746cca7eaa@o296332.ingest.sentry.io/1546547"), window.APPSMITH_FEATURE_CONFIGS = { sentry: { dsn: parseConfig("https://abf15a075d1347969df44c746cca7eaa@o296332.ingest.sentry.io/1546547"), release: parseConfig(""), environment: parseConfig("Production") }, smartLook: { id: parseConfig("c370af0df0edf38360adbefbdc47d2b42ea137c9") }, enableRapidAPI: parseConfig(""), segment: { apiKey: parseConfig("9OnZ6LnDztuZZo4zXfoutEEBB2wftHUH"), ceKey: parseConfig("AGuvNK0MfQgtxwBMCia7fRNxTjfoBRec") }, fusioncharts: { licenseKey: parseConfig("5bD2nuyE1C3C11D3E3E3C1G3F2I4B3C8xgaD3D2E3qG2A1A4dgrE2F4D1G-7xoD1D3D1lttA32B2B9C3B5D2C4B1D3H4A9kcC-13E3D4HH2sudB2D3D2A-9hH1CB5B8dfwB4G1H4I2A3C2C1D6B1E1C4G1C3B5o==") }, enableMixpanel: parseConfig("9OnZ6LnDztuZZo4zXfoutEEBB2wftHUH"), algolia: { apiId: parseConfig("AZ2Z9CJSJ0"), apiKey: parseConfig("dfde934d9bdc2e0b14830f1dd3cb240f"), indexName: parseConfig("omnibar_docusaurus_index") }, logLevel: CONFIG_LOG_LEVEL_INDEX > -1 ? LOG_LEVELS[CONFIG_LOG_LEVEL_INDEX] : LOG_LEVELS[1], cloudHosting: CLOUD_HOSTING, enableTNCPP: parseConfig("true"), appVersion: { id: parseConfig(""), releaseDate: parseConfig("") }, intercomAppID: INTERCOM_APP_ID, mailEnabled: parseConfig("true"), cloudServicesBaseUrl: parseConfig("https://cs.appsmith.com") || "https://cs.appsmith.com", googleRecaptchaSiteKey: parseConfig("6LfXDPIaAAAAAKUUoWxXi35KN7VIlXoD2rtI5gg8"), hideWatermark: parseConfig(""), disableIframeWidgetSandbox: parseConfig("true"), customerPortalUrl: parseConfig("") || "https://customer.appsmith.com", pricingUrl: parseConfig("") || "https://www.appsmith.com/pricing", airGapped: parseConfig("false") }</script>
</body>

</html>

With script in the end of <body>, the main bundle takes 3s (request 6):

waterfall (22)

HTML code
<!doctype html>
<html lang="en">

<head>
    <script>navigator.serviceWorker.register = () => { };</script>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
    <title>Appsmith</title>
    <style>
        #loader {
            position: fixed;
            left: 0;
            top: 0;
            height: 4px;
            background: #d7d7d7;
            transition: all ease-in .3s
        }
    </style>
    <link rel="preload" as="script" href="/static/js/2209.1a60b0b6.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/55178.ef82f381.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/81621.58d65afe.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/89532.5ad27563.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/85342.fd2d6599.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/3638.68e4f960.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/51863.ac47ac64.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/global-search.3bd25387.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/82492.1f73e40c.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/66124.a789a752.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/7207.9189ae78.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/29020.74dfd06a.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/72527.ce5b870f.chunk.js" fetchpriority="low" />
    <link rel="preload" as="style" href="/static/css/editor.6f95ab40.chunk.css" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/editor.8486711d.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/2209.1a60b0b6.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/55178.ef82f381.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/51863.ac47ac64.chunk.js" fetchpriority="low" />
    <link rel="preload" as="script" href="/static/js/global-search.3bd25387.chunk.js" fetchpriority="low" />
    <script>const parseConfig = e => { if (0 === e.indexOf("__") || 0 === e.indexOf("$") || 0 === e.indexOf("%")) return ""; const o = e.trim(); return "false" !== o.toLowerCase() && "" !== o && ("true" === o.toLowerCase() || o) }, CLOUD_HOSTING = parseConfig("true"), ZIPY_KEY = parseConfig(""), AIRGAPPED = parseConfig("false"); if (CLOUD_HOSTING && ZIPY_KEY && !AIRGAPPED) { const e = document.createElement("script"); e.crossOrigin = "anonymous", e.defer = !0, e.src = "https://cdn.zipy.ai/sdk/v1.0/zipy.min.umd.js", e.onload = () => { window.zipy && window.zipy.init(ZIPY_KEY) }; const o = document.getElementsByTagName("head")[0]; o && o.appendChild(e) } gapiLoaded = () => { window.googleAPIsLoaded = !0 }, onError = () => { window.googleAPIsLoaded = !1 }</script>
    <script async defer="defer" id="googleapis" src="https://apis.google.com/js/api.js" onload="gapiLoaded()"
        onerror="onError()"></script>
    <link href="/static/css/main.c3722dd4.css" rel="stylesheet">
</head>

<body class="appsmith-light-theme"><noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="loader" style="width:30vw"></div>
    <div id="header-root"></div>
    <div id="root"></div>
    <script
        type="text/javascript">const getIsLocalStorageSupported = () => { try { return window.localStorage.setItem("test", "testA"), window.localStorage.removeItem("test"), !0 } catch (e) { return !1 } }, isLocalStorageSupported = getIsLocalStorageSupported(), handleLocalStorageNotSupportedError = () => { console.error("Localstorage storage is not supported on your device.") }, localStorageUtil = { getItem: e => { if (isLocalStorageSupported) return window.localStorage.getItem(e); handleLocalStorageNotSupportedError() }, removeItem: e => { if (isLocalStorageSupported) return window.localStorage.removeItem(e); handleLocalStorageNotSupportedError() }, setItem: (e, r) => { if (isLocalStorageSupported) return window.localStorage.setItem(e, r); handleLocalStorageNotSupportedError() } }; window.addEventListener("DOMContentLoaded", (e => { document.getElementById("loader").style.width = "50vw" })); const registerPageServiceWorker = () => { "serviceWorker" in navigator && !window.Cypress && window.addEventListener("load", (function () { navigator.serviceWorker.register("/pageService.js").catch((e => { console.error("Service Worker Registration failed: " + e) })) })) }; "serviceWorker" in navigator && !window.Cypress && window.addEventListener("load", (function () { navigator.serviceWorker.register("/pageService.js").catch((e => { console.error("Service Worker Registration failed: " + e) })) }))</script>
    <script
        type="text/javascript">const LOG_LEVELS = ["debug", "error"], CONFIG_LOG_LEVEL_INDEX = LOG_LEVELS.indexOf(parseConfig("")), INTERCOM_APP_ID = parseConfig("%REACT_APP_INTERCOM_APP_ID%") || parseConfig("y10e7138"), DISABLE_INTERCOM = parseConfig("") || parseConfig("false"); INTERCOM_APP_ID.length && !DISABLE_INTERCOM && function () { var _ = window, e = _.Intercom; if ("function" == typeof e) e("reattach_activator"), e("update", _.intercomSettings); else { var E = document, a = function () { a.c(arguments) }; a.q = [], a.c = function (_) { a.q.push(_) }, _.Intercom = a; var I = function () { var _ = E.createElement("script"); _.type = "text/javascript", _.async = !0, _.src = "https://widget.intercom.io/widget/" + INTERCOM_APP_ID; var e = E.getElementsByTagName("script")[0]; e.parentNode.insertBefore(_, e) }; "complete" === document.readyState ? I() : _.attachEvent ? _.attachEvent("onload", I) : _.addEventListener("load", I, !1) } }(), window.SENTRY_CONFIG = parseConfig("https://abf15a075d1347969df44c746cca7eaa@o296332.ingest.sentry.io/1546547"), window.APPSMITH_FEATURE_CONFIGS = { sentry: { dsn: parseConfig("https://abf15a075d1347969df44c746cca7eaa@o296332.ingest.sentry.io/1546547"), release: parseConfig(""), environment: parseConfig("Production") }, smartLook: { id: parseConfig("c370af0df0edf38360adbefbdc47d2b42ea137c9") }, enableRapidAPI: parseConfig(""), segment: { apiKey: parseConfig("9OnZ6LnDztuZZo4zXfoutEEBB2wftHUH"), ceKey: parseConfig("AGuvNK0MfQgtxwBMCia7fRNxTjfoBRec") }, fusioncharts: { licenseKey: parseConfig("5bD2nuyE1C3C11D3E3E3C1G3F2I4B3C8xgaD3D2E3qG2A1A4dgrE2F4D1G-7xoD1D3D1lttA32B2B9C3B5D2C4B1D3H4A9kcC-13E3D4HH2sudB2D3D2A-9hH1CB5B8dfwB4G1H4I2A3C2C1D6B1E1C4G1C3B5o==") }, enableMixpanel: parseConfig("9OnZ6LnDztuZZo4zXfoutEEBB2wftHUH"), algolia: { apiId: parseConfig("AZ2Z9CJSJ0"), apiKey: parseConfig("dfde934d9bdc2e0b14830f1dd3cb240f"), indexName: parseConfig("omnibar_docusaurus_index") }, logLevel: CONFIG_LOG_LEVEL_INDEX > -1 ? LOG_LEVELS[CONFIG_LOG_LEVEL_INDEX] : LOG_LEVELS[1], cloudHosting: CLOUD_HOSTING, enableTNCPP: parseConfig("true"), appVersion: { id: parseConfig(""), releaseDate: parseConfig("") }, intercomAppID: INTERCOM_APP_ID, mailEnabled: parseConfig("true"), cloudServicesBaseUrl: parseConfig("https://cs.appsmith.com") || "https://cs.appsmith.com", googleRecaptchaSiteKey: parseConfig("6LfXDPIaAAAAAKUUoWxXi35KN7VIlXoD2rtI5gg8"), hideWatermark: parseConfig(""), disableIframeWidgetSandbox: parseConfig("true"), customerPortalUrl: parseConfig("") || "https://customer.appsmith.com", pricingUrl: parseConfig("") || "https://www.appsmith.com/pricing", airGapped: parseConfig("false") }</script>
    <script src="/static/js/main.c65af675.js"></script>
</body>

</html>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that the script is in the end of <head> in the first test. We can move it to the beginning of <head>, but that’s

  1. a more complex build setup (HTMLWebpackPlugin doesn’t support that as a flag),
  2. doesn’t even change the bundle’s download time (req. 2):

waterfall (23)

htmlWebpackPlugin.userOptions.scriptLoading = "blocking";

return webpackConfig;
},
},
},
// Emit dedicated HTML files for edit and view modes. This is done as an optimization (to preload
// route-specific chunks on the most critical routes) and doesn’t affect the actual app behavior.
{
Expand Down
23 changes: 13 additions & 10 deletions app/client/packages/design-system/widgets-old/src/Tooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ export type TooltipProps = CommonComponentProps & {
styles?: any;
};

const rootElementId = "tooltip-root";

let portalContainer = document.getElementById(rootElementId);

const TooltipWrapper = styled(Tooltip)<
PropsWithChildren<{
width?: string;
Expand All @@ -65,11 +61,18 @@ const TooltipWrapper = styled(Tooltip)<
}
`;

if (!portalContainer) {
const tooltipPortalElement = document.createElement("div");
tooltipPortalElement.id = rootElementId;
document.body.append(tooltipPortalElement);
portalContainer = document.getElementById(rootElementId);
function getPortalContainer() {
const rootElementId = "tooltip-root";
let portalContainer = document.getElementById(rootElementId);

if (!portalContainer) {
const tooltipPortalElement = document.createElement("div");
tooltipPortalElement.id = rootElementId;
document.body.append(tooltipPortalElement);
portalContainer = tooltipPortalElement;
}

return portalContainer;
}

function TooltipComponent(props: TooltipProps) {
Expand Down Expand Up @@ -97,7 +100,7 @@ function TooltipComponent(props: TooltipProps) {
popoverClassName={`${GLOBAL_STYLE_TOOLTIP_CLASSNAME} ${
props.popoverClassName ?? ""
}`}
portalContainer={portalContainer as HTMLDivElement}
portalContainer={getPortalContainer()}
position={props.position}
transitionDuration={props.transitionDuration || 0}
underline={props.underline}
Expand Down
3 changes: 2 additions & 1 deletion app/client/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
}

return chunksToPreload.flatMap(i => [...i.files])
.map(url => `<link rel="preload" as="${getPreloadValueForFile(url)}" href="${webpackConfig.output.publicPath + url}" />`)
// `fetchpriority="low"` ensures preloads don’t compete for bandwidth with the main script: https://3perf.slack.com/archives/C01SGCF8PM0/p1684511126862229?thread_ts=1684466888.430869&cid=C01SGCF8PM0
.map(url => `<link rel="preload" fetchpriority="low" as="${getPreloadValueForFile(url)}" href="${webpackConfig.output.publicPath + url}" />`)
.join('\n')

function getPreloadValueForFile(fileName) {
Expand Down
22 changes: 17 additions & 5 deletions app/client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ import AppErrorBoundary from "./AppErrorBoundry";
const shouldAutoFreeze = process.env.NODE_ENV === "development";
setAutoFreeze(shouldAutoFreeze);

runSagaMiddleware();

appInitializer();

function App() {
return (
<Sentry.ErrorBoundary fallback={"An error has occured"}>
Expand Down Expand Up @@ -59,7 +55,23 @@ const mapStateToProps = (state: AppState) => ({

const ThemedAppWithProps = connect(mapStateToProps)(ThemedApp);

ReactDOM.render(<App />, document.getElementById("root"));
const initAndRenderApp = () => {
runSagaMiddleware();

appInitializer();

ReactDOM.render(<App />, document.getElementById("root"));
};

// Some code of the app depends on the whole DOM being ready
if (
document.readyState === "interactive" ||
document.readyState === "complete"
) {
initAndRenderApp();
} else {
document.addEventListener("DOMContentLoaded", initAndRenderApp);
}

// expose store when run in Cypress
if ((window as any).Cypress) {
Expand Down