为什么我使用 Next.js (React) 构建的 Shopify 应用加载速度如此之慢?
Why is my Shopify App built with Next.js (React) so slow to load?
我遵循了这个教程:https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react
从一开始,我的应用程序加载速度非常慢,包括更改选项卡时,包括通过 ngrok 加载和 运行 在本地主机上或部署在应用程序引擎上时。
可能是什么原因造成的?
P.S.: 我是 React、Next.js 和 Shopify App 开发的新手,所以答案可能很基础。
P.P.S.: 基于红色,构建输出似乎表明“所有人共享的首次加载 JS”太大。我不知道如何对此进行调查并减小所述块的大小,尽管仅仅 214KB 无法解释如此缓慢的加载时间,可以吗?
建造
React 开发工具分析器
@next/bundle-analyzer 输出:
已解析
压缩
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 开发工具网络选项卡
编辑:
npm run dev
由于某些原因,“webpack-hmr”行加载时间不断增加。
npm run build && npm run start
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(客户端)
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: "..."};
}
});
});
编辑
我已经提供了初步答案,但如果可能,我正在寻找更好的答案。
此外,似乎可以使 Shopify 应用桥接导航链接使用 next.js 路由器而不是触发整页重新加载:
https://shopify.dev/tools/app-bridge/actions/navigation
如果有人足够详细地为 next.js 分享如何做到这一点,那会比我的回答更好。
根据您的开发工具瀑布,您对索引的初始加载仅花费了将近 2 秒的时间,而数据仅为 18.5KB。这速度慢得惊人,甚至在你的其他资源还没有达到之前。我的第一个想法是 network/server 滞后。您是在本地托管还是在某种网络服务器上托管?
我会尽可能地剥离它,甚至可能只是尝试加载一个只有 header 的简单 index.html 文件。如果加载需要几秒钟,那么您可能需要升级或迁移到更好的主机。如果您在本地托管,这可能只是您的互联网上传速度慢的问题。许多互联网计划下载速度快但上传速度慢,您并不总是能得到 ISP 的承诺。
尝试通过删除任何不必要的代码来优化您的代码。尝试使用更多的动态导入,这样您就可以快速加载初始 bioler 板,并在客户端稍后加载图表、图形、图片和视频等繁重的代码。
从“next/dynamic”导入动态,这应该像 youtube 一样为客户提供快速的首次绘制视图。
https://nextjs.org/docs/advanced-features/dynamic-import
尝试使用 React Formik(针对小型应用程序的优化表单控制器)并尝试在 Class 组件上使用函数组件。使用 Next,您可以在 getStatiProps、getServerSideProps、getStaticPaths 中执行大部分数据库调用。对于定期缓存获取,使用 SWR 挂钩。
我在我的问题中分享了使用 npm 运行 dev 测量的加载时间,但这里也有一些关于生产模式下加载时间的信息。
运行 prod 模式下的应用(npm run build
和 npm run start
)在删除嵌入到 Shopify 后台后 UI 显示该应用总共需要大约在 prod 模式下加载 2 秒,这看起来仍然很慢(Shopify UI 增加了大约 3 秒)。
点击 Shopify 应用新娘导航链接时会重新加载整页,而不是像 Next.js 链接那样更改页面。
用 Next 链接替换了 App Navigation 链接。
不过,第一次加载 1.86 秒还是很慢,我愿意寻求更好的解决方案。
我遵循了这个教程:https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react
从一开始,我的应用程序加载速度非常慢,包括更改选项卡时,包括通过 ngrok 加载和 运行 在本地主机上或部署在应用程序引擎上时。
可能是什么原因造成的?
P.S.: 我是 React、Next.js 和 Shopify App 开发的新手,所以答案可能很基础。
P.P.S.: 基于红色,构建输出似乎表明“所有人共享的首次加载 JS”太大。我不知道如何对此进行调查并减小所述块的大小,尽管仅仅 214KB 无法解释如此缓慢的加载时间,可以吗?
建造
React 开发工具分析器
@next/bundle-analyzer 输出:
已解析
压缩
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 开发工具网络选项卡
编辑:
npm run dev
由于某些原因,“webpack-hmr”行加载时间不断增加。
npm run build && npm run start
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(客户端)
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: "..."};
}
});
});
编辑
我已经提供了初步答案,但如果可能,我正在寻找更好的答案。
此外,似乎可以使 Shopify 应用桥接导航链接使用 next.js 路由器而不是触发整页重新加载:
https://shopify.dev/tools/app-bridge/actions/navigation
如果有人足够详细地为 next.js 分享如何做到这一点,那会比我的回答更好。
根据您的开发工具瀑布,您对索引的初始加载仅花费了将近 2 秒的时间,而数据仅为 18.5KB。这速度慢得惊人,甚至在你的其他资源还没有达到之前。我的第一个想法是 network/server 滞后。您是在本地托管还是在某种网络服务器上托管?
我会尽可能地剥离它,甚至可能只是尝试加载一个只有 header 的简单 index.html 文件。如果加载需要几秒钟,那么您可能需要升级或迁移到更好的主机。如果您在本地托管,这可能只是您的互联网上传速度慢的问题。许多互联网计划下载速度快但上传速度慢,您并不总是能得到 ISP 的承诺。
尝试通过删除任何不必要的代码来优化您的代码。尝试使用更多的动态导入,这样您就可以快速加载初始 bioler 板,并在客户端稍后加载图表、图形、图片和视频等繁重的代码。 从“next/dynamic”导入动态,这应该像 youtube 一样为客户提供快速的首次绘制视图。
https://nextjs.org/docs/advanced-features/dynamic-import
尝试使用 React Formik(针对小型应用程序的优化表单控制器)并尝试在 Class 组件上使用函数组件。使用 Next,您可以在 getStatiProps、getServerSideProps、getStaticPaths 中执行大部分数据库调用。对于定期缓存获取,使用 SWR 挂钩。
我在我的问题中分享了使用 npm 运行 dev 测量的加载时间,但这里也有一些关于生产模式下加载时间的信息。
运行 prod 模式下的应用(npm run build
和 npm run start
)在删除嵌入到 Shopify 后台后 UI 显示该应用总共需要大约在 prod 模式下加载 2 秒,这看起来仍然很慢(Shopify UI 增加了大约 3 秒)。
点击 Shopify 应用新娘导航链接时会重新加载整页,而不是像 Next.js 链接那样更改页面。
用 Next 链接替换了 App Navigation 链接。
不过,第一次加载 1.86 秒还是很慢,我愿意寻求更好的解决方案。