skip to Main Content

I followed this tutorial: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react

From the beginning, my app was extremely slow to load, including when changing tabs, including when loaded through ngrok and run on localhost or deployed on app engine.

What could be causing this ?

P.S.: I am new to React, Next.js and Shopify App development, so the answer could be quite basic.

P.P.S.: The build output seems to indicate "First Load JS shared by all" is too large based on the red color. I do not know how to investigate this and reduce the size of said chunks although a mere 214KB could not explain such slow load times, could it ?


Build

enter image description here


React Dev Tools Profiler

enter image description here

enter image description here


@next/bundle-analyzer Output:

Parsed

enter image description here

enter image description here

Gzipped

enter image description here

enter image description here


package.json

 {
      "name": "ShopifyApp1",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo "Error: no test specified" && exit 1",
        "dev": "node server.js NODE_ENV=dev",
        "build": "next build",
        "deploy": "next build && gcloud app deploy --version=deploy",
        "start": "NODE_ENV=production node server.js",
        "analyze": "cross-env ANALYZE=true npm run build"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@google-cloud/storage": "^5.2.0",
        "@next/bundle-analyzer": "^9.5.2",
        "@sendgrid/mail": "^7.2.3",
        "@shopify/app-bridge-react": "^1.26.2",
        "@shopify/koa-shopify-auth": "^3.1.65",
        "@shopify/koa-shopify-graphql-proxy": "^4.0.1",
        "@shopify/koa-shopify-webhooks": "^2.4.3",
        "@shopify/polaris": "^5.1.0",
        "@zeit/next-css": "^1.0.1",
        "apollo-boost": "^0.4.9",
        "cors": "^2.8.5",
        "cross-env": "^7.0.2",
        "dotenv": "^8.2.0",
        "email-validator": "^2.0.4",
        "extract-domain": "^2.2.1",
        "firebase-admin": "^9.0.0",
        "graphql": "^15.3.0",
        "helmet": "^4.0.0",
        "isomorphic-fetch": "^2.2.1",
        "js-cookie": "^2.2.1",
        "koa": "^2.13.0",
        "koa-body": "^4.2.0",
        "koa-bodyparser": "^4.3.0",
        "koa-helmet": "^5.2.0",
        "koa-router": "^9.1.0",
        "koa-session": "^6.0.0",
        "next": "^9.5.1",
        "react": "^16.13.1",
        "react-apollo": "^3.1.5",
        "react-dom": "^16.13.1",
        "react-infinite-scroll-component": "^5.0.5",
        "sanitize-html": "^1.27.2",
        "scheduler": "^0.19.1",
        "store-js": "^2.0.4",
        "tldts": "^5.6.46"
      },
      "devDependencies": {
        "webpack-bundle-analyzer": "^3.8.0",
        "webpack-bundle-size-analyzer": "^3.1.0"
      },
      "browser": {
        "@google-cloud/storage": false,
        "@sendgrid/mail": false,
        "@shopify/koa-shopify-auth": false,
        "@shopify/koa-shopify-graphql-proxy": false,
        "@shopify/koa-shopify-webhooks": false,
        "cors": false,
        "email-validator": false,
        "extract-domain": false,
        "firebase-admin": false,
        "graphql": false,
        "helmet": false,
        "isomorphic-fetch": false,
        "koa": false,
        "koa-body": false,
        "koa-bodyparser": false,
        "koa-helmet": false,
        "koa-router": false,
        "koa-session": false,
        "sanitize-html": false,
        "tldts": false
      }
    }

Chrome Dev Tools Network Tab

EDIT:

npm run dev

enter image description here

For some reason, the "webpack-hmr" line load time keeps constantly increasing.

npm run build && npm run start

enter image description here


next.config.js

require("dotenv").config({path:"live.env"});
const withCSS = require('@zeit/next-css');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const withBundleAnalyzer = require('@next/bundle-analyzer')({enabled: process.env.ANALYZE === 'true'})
const apiKey =  JSON.stringify(process.env.SHOPIFY_API_KEY);
module.exports =  withBundleAnalyzer(
  withCSS({
    distDir: 'build',
    webpack: (config) => {
      const env = { API_KEY: apiKey };
      config.plugins.push(new webpack.DefinePlugin(env));
      config.plugins.push(new webpack.DefinePlugin(new BundleAnalyzerPlugin()));
      config.resolve = {
        alias: {
          'react-dom$': 'react-dom/profiling',
          'scheduler/tracing': 'scheduler/tracing-profiling'
        },
        ...config.resolve
      };
      return config;
    }
  })
);

_app.js

import App from 'next/app';
import Head from 'next/head';
import { AppProvider } from '@shopify/polaris';
import { Provider } from '@shopify/app-bridge-react';
import '@shopify/polaris/dist/styles.css'
import translations from '@shopify/polaris/locales/en.json';
import Cookies from 'js-cookie';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';

const client = new ApolloClient({
  fetchOptions: {
    credentials: 'include'
  },
});

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props;
    const config = { apiKey: API_KEY, shopOrigin: Cookies.get("shopOrigin"), forceRedirect: true };
    return (
      <React.Fragment>
        <Head>
          <title>...</title>
          <meta charSet="utf-8" />
        </Head>
        <Provider config={config}>
          <AppProvider i18n={translations}>
            <ApolloProvider client={client}>
              <Component {...pageProps} />
            </ApolloProvider>
          </AppProvider>
        </Provider>
      </React.Fragment>
    );
  }
}

export default MyApp;

Index.js (client)

import {
  Button,
  Card,
  Form,
  FormLayout,
  Layout,
  Page,
  Stack,
  TextField,
  DisplayText,
  Toast,
  Frame
} from '@shopify/polaris';

class Index extends React.Component  {

  state = {
    emails: '',
    domain: '' ,
    alias: '',
    err: '',
    message: '',
    active: false,
    loadingDomainResponse: false,
    loadingEmailResponse: false
  };

  componentDidMount() {
    fetch(`/state`, {
      method: 'GET'
    }).then(response => response.json())
    .then(result => {
      if (result.err) {
        this.setState({
          err: result.err,
          message: result.err,
          active: true
        })
      }
      else {
        this.setState({
          emails: result.emails,
          domain: result.domain,
          alias: result.alias
        })
      }
    });
  };

  

  render() {

    const { emails, domain, alias, err, message, active, loadingEmailResponse, loadingDomainResponse} = this.state;
    

    const toastMarkup = active ? (
      <Toast content={message} error={err} onDismiss={this.handleToast}/> 
    ) : null;


    return (
      <Frame>
         <Page>
          {toastMarkup}
          <Layout>
            <Layout.AnnotatedSection
              title="..."
              description="..."
            >
              <Card sectioned>
                <Form onSubmit={this.handleSubmitEmails}>
                  <FormLayout>
                    <TextField
                      value={emails}
                      onChange={this.handleChange('emails')}
                      label="..."
                      type="emails"
                      maxlength="200"
                    />
                    <Stack distribution="trailing">
                      <Button primary submit loading={loadingEmailResponse}>
                        Save
                      </Button>
                    </Stack>
                  </FormLayout>
                </Form>
              </Card>
            </Layout.AnnotatedSection>
            <Layout.AnnotatedSection
              title="..."
              description="..."
            >
              <Card sectioned>
                <DisplayText size="small"> {domain} </DisplayText>
                <br/>
                <Form onSubmit={this.handleSubmitDomain}>
                  <FormLayout>
                    <TextField
                      value={alias}
                      onChange={this.handleChange('alias')}
                      label="..."
                      type="text"
                      maxlength="50"
                    />
                    <Stack distribution="trailing">
                      <Button primary submit loading={loadingDomainResponse}>
                        Save
                      </Button>
                    </Stack>
                  </FormLayout>
                </Form>
              </Card>
            </Layout.AnnotatedSection>
          </Layout> 
       </Page>
      </Frame>
    );
  }

  handleToast = () => {
    this.setState({
      err: false,
      message: false,
      active: false
    })
  };
  
  handleSubmitEmails = () => {
    this.setState({loadingEmailResponse:true});
    fetch(`/emails`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        emails: this.state.emails
      })
    }).then(response => response.json())
    .then(result => {
      console.log("JSON: "+JSON.stringify(result));
      if (result.err) {
        this.setState({
          err: result.err,
          message: result.err,
          active: true
        })
      }
      else {
        this.setState({message: "...", active: true});
      }
      this.setState({loadingEmailResponse:false});
    });
  };

  handleSubmitDomain = () => {
    this.setState({loadingDomainResponse:true});
    fetch(`/domain`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: 
        JSON.stringify({
          alias: this.state.alias
        })
      }).then(response => response.json())
      .then(result => {
        console.log("JSON: "+JSON.stringify(result));
        if (result.err) {
          this.setState({
            err: result.err,
            message: result.err,
            active: true
          })
        }
        else {
          this.setState({message: "...", active: true});
        }
        this.setState({loadingDomainResponse:false});
      });
  };

  


  handleChange = (field) => {
    return (value) => this.setState({ [field]: value });
  };
}

export default Index;

server.js

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
app.prepare().then(() => {
    const server = new Koa();
    const router = new Router();
    server.use(bodyParser());
    server.use(session({ secure: true, sameSite: 'none' }, server));
    server.keys = [SHOPIFY_API_SECRET_KEY];
    router.get('/state',  async (ctx) => {
        let domain = ctx.session.shop;
        let alias;
        const snap = await global.db.collection("...").doc(ctx.session.shop).get();
            if (snap.data().alias) {
                alias = snap.data().alias;
            }
            let emails = snap.data().emails;
            let emailString = "";
            if (!emails) {
                ctx.response.body = {err: "..."};
            }
            else if(emails.length < 4) {
                for (email of emails) {
                    emailString += (","+email);
                }
                theEmailString = emailString.substring(1);
                let response = {
                    domain: domain,
                    alias: alias,
                    emails: theEmailString
                }
                ctx.response.body = response;
            } 
            else {
                ctx.response.body = {err: "..."};
            }
    });
});

Edit

I have provided an initial answer, but I am looking for a better one if possible.

Also, it seems possible to make the Shopify app bridge navigation links use the next.js router instead of triggering full page reloads:

https://shopify.dev/tools/app-bridge/actions/navigation

If someone shares how to do that for next.js with sufficient detail, that would be better than my answer.

3

Answers


  1. Chosen as BEST ANSWER

    enter image description here

    I shared load times measured using npm run dev in my question but here is some information about load times in prod mode as well.

    Running the app in prod mode (npm run build and npm run start) after removing the embedding into the Shopify admin UI shows that the app takes a total of about 2 seconds to load in prod mode which still seems very slow (Shopify UI was adding about 3 seconds).

    The Shopify app bride navigation links would do full page reloads when clicked instead of changing pages like Next.js links would.

    Replaced the App Navigation links with Next links.

    Still, 1.86 seconds for the first load is very slow, and am open to better solutions.


  2. Your initial load on index, according your dev tools waterfall, took almost 2 seconds for only 18.5KB of data. This is alarmingly slow and prior to the rest of your resources being reached even. My first thought would be network/server lag. Are you hosting this locally or on a web server of some sort?

    I would strip it down as much as you can, maybe even just try and load a simple index.html file with only a header. If that takes a couple of seconds to load then you may need to either upgrade or migrate to a better host. If you are hosting locally this could just be an issue of your internet having a low upload speed. Many internet plans have fast downloads but slow uploads and you are not always going to get what your ISP promises.

    Login or Signup to reply.
  3. try optimizing your code by removing any unnecessary code. Try to use more dynamic import such that you get a fast initial bioler plate load up and the heavy codes like charts, graphs and pictures and video load at a later time at the client end.
    import dynamic from "next/dynamic" , this should give the client a quick first paint view like youtube does.

    https://nextjs.org/docs/advanced-features/dynamic-import
    

    try out react Formik( An optimize form controler for small apps) and also try using function components over Class Components. Using Next you can do most of you database calls in getStatiProps, getServerSideProps, getStaticPaths. for periodic cached fetching use SWR hook.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search