Closed nibtime closed 1 year ago
We're a little ways off from totally taking care of this problem. But I've opened PR #773 to hopefully mitigate future bloat.
I think a major win would be to take better advantage of dynamic imports. Codemirror and Prosemirror are pretty big, if we could split the wysiwyg into it's own chunk that would be quite helpful. I've done this for apps before, but I'm a little nervous about doing it for lbraries.
Good news, I found a way to hotfix this! App bundle size is now just around 490KB (that's like 1.2MB smaller 😃 ) and Tina is still up and running in development. The commons
bundle generated by Gatsby is also 220KB smaller, it now just contains react-dom
and schduler
. Tina adds additional dependencies there as well, most notably esprima.js
and js-yaml
.
And the hotfix of course. It involves 2 parts:
js-conditional-compile-loader
Install js-conditional-compile-loader
with yarn add js-conditional-compile-loader -D
Then configure the loader in onCreateWebpackConfig
import { CreateWebpackConfigArgs } from "gatsby"
import path from "path"
const conditionalCompilerRule = [
{
test: /\.(ts|tsx)$/,
include: [path.resolve(`${__dirname}/../../src`)],
use: [
"babel-loader?cacheDirectory",
{
loader: "js-conditional-compile-loader",
options: {
isDebug: process.env.NODE_ENV === "development"
}
}
]
}
]
export const onCreateWebpackConfig = ({ actions, getConfig }: CreateWebpackConfigArgs) => {
const config = getConfig()
config.module.rules = [...conditionalCompilerRule, ...config.module.rules]
actions.replaceWebpackConfig(config)
}
Now you can define code, that will just be present in the development build. Make sure that every piece of Tina code is guarded. Looks like this:
UPDATE: I found a better way to approach this, see last section.
/* IFDEBUG */
import { remarkForm } from "gatsby-tinacms-remark"
/* FIDEBUG */
...
let notFoundPage = NotFoundPage
/* IFDEBUG */
notFoundPage = remarkForm(NotFoundPage, { label: "404 Page", fields: basePageFields })
/* FIDEBUG */
// tslint:disable-next-line: export-name
export default notFoundPage
/* IFDEBUG */
import { useLocalRemarkForm } from "gatsby-tinacms-remark"
/* FIDEBUG */
...
/* IFDEBUG */
useLocalRemarkForm(data.pageData as any, { label: "Membership Page", fields: basePageFields })
useLocalRemarkForm(data.membershipData as any, {
label: "Membership Plans",
fields: membershipPlansFields
})
/* FIDEBUG */
This is pretty straightforward. Just assign the Tina plugin configuration to a variable and then match on NODE_ENV.
const tinacms = {
resolve: "gatsby-plugin-tinacms",
options: {
sidebar: {
hidden: process.env.NODE_ENV === "production",
position: "displace"
},
plugins: [
"gatsby-tinacms-remark",
"gatsby-tinacms-json",
{
resolve: "gatsby-tinacms-git",
options: {
/**
* Must be absolute path to root of workspace / repository
*/
pathToRepo: WORKSPACE_ROOT,
/**
* Must be unprefixed path to Gatsby project, relative to repository root
*/
pathToContent: "frontend"
}
}
]
}
}
export const plugins: Plugins = [
"gatsby-plugin-typescript",
"gatsby-plugin-tslint",
"gatsby-plugin-react-helmet",
"gatsby-plugin-sass",
...(process.env.CI_APPLICATION_ENVIRONMENT === "development_local" ? [bundleAnalyzer] : []),
...(process.env.NODE_ENV === "development" ? [tinacms] : []),
Now there's one last thing to do. GraphQL queries using custom fields / fragments required by Tina would fail at this point in production, since we excluded the Tina plugins to provide them.
So need to provide them ourselves. I basically just copy+pasted this from the plugins' code into setFieldsOnGraphQLNodeType
of my project.
import { SetFieldsOnGraphQLNodeTypeArgs } from "gatsby"
import { GraphQLString } from "gatsby/graphql"
import slash from "slash"
const setTinaRemarkFields = ({ type }: SetFieldsOnGraphQLNodeTypeArgs) => {
const pathRoot = slash(process.cwd())
const hasMarkdown = !!type.nodes.find(node => node.internal.owner === "gatsby-transformer-remark")
if (hasMarkdown) {
return {
rawFrontmatter: {
type: GraphQLString,
resolve: source => {
return JSON.stringify(source.frontmatter)
}
},
fileRelativePath: {
type: GraphQLString,
resolve: source => {
return source.fileAbsolutePath.replace(pathRoot, "")
}
}
}
}
// by default return empty object
return {}
}
const setTinaJsonFields = ({ type, getNode }: SetFieldsOnGraphQLNodeTypeArgs) => {
const pathRoot = slash(process.cwd())
const hasJson = !!type.nodes.find(node => node.internal.owner === "gatsby-transformer-json")
if (!hasJson) {
return {}
}
return {
rawJson: {
type: GraphQLString,
args: {},
resolve: ({ children, id, internal, parent, fields, ...data }) => {
return JSON.stringify(data)
}
},
fileRelativePath: {
type: GraphQLString,
args: {},
resolve: ({ parent }) => {
const p = getNode(parent)
return p.absolutePath.replace(pathRoot, "")
}
}
}
}
export const setFieldsOnGraphQLNodeType = (args: SetFieldsOnGraphQLNodeTypeArgs): any => {
if (process.env.NODE_ENV === "production") {
const jsonFields = setTinaRemarkFields(args)
const remarkFields = setTinaJsonFields(args)
return { ...jsonFields, ...remarkFields }
}
return {}
}
Now you should have a production bundle without the penalty from Tina + dependencies. 🎉
I was happy with the result, but I didn't like that the conditional compilation stuff leaked into application code. This can be mitigated by a very simple trick. I created some sort of proxies for the HOCs and hooks from Tina in a local module that have the conditional compilation baked in and then use them as a drop-in replacement. Looks like this:
/* IFDEBUG */
import { remarkForm as tinaRemarkForm } from "gatsby-tinacms-remark"
/* FIDEBUG */
export const remarkForm: typeof tinaRemarkForm = (Component, options) => {
let returnValue: ReturnType<typeof tinaRemarkForm> = Component
/* IFDEBUG */
returnValue = tinaRemarkForm(Component, options)
/* FIDEBUG */
return returnValue
}
/* IFDEBUG */
import { useLocalRemarkForm as tinaUseLocalRemarkForm } from "gatsby-tinacms-remark"
/* FIDEBUG */
export const useLocalRemarkForm: typeof tinaUseLocalRemarkForm = (remarkNode, formOverrides) => {
let returnValue: ReturnType<typeof tinaUseLocalRemarkForm> = [undefined, undefined]
/* IFDEBUG */
returnValue = tinaUseLocalRemarkForm(remarkNode, formOverrides)
/* FIDEBUG */
return returnValue
}
This way, existing Tina code does not have to be modified, just the import location needs to be changed. After the bundle size problem has been fixed in Tina, the traces of the hack can be removed very easily by removing the stuff from Gatsby APIs and change the proxies so they just reexport from Tina.
export { useLocalRemarkForm } from "gatsby-tinacms-remark"
You're a real trooper @nibtime 🚀
Rest assured we want a slightly easier approach then this.
@ncphillips it's a dirty hack obviously and should not become an official solution 😅. Although it would be possible to refine this and abstract the ugliness of conditional compilation away and reexport HOCs and hooks of Tina with conditional compilation baked-in. The whole thing could then probably also be extracted into a Gatsby theme, including the webpack and plugin configuration part (I've never authored one so far though, just roughly know the concepts).
Of course a solution on library-level, e.g. with dynamic imports would be highly preferable, although I could imagine this might require a lot of effort.
This is a WIN! I'll be implementing this ASAP while we wait for the official solution. Thanks for sharing @nibtime 🍻
Is there any more work being planned to improve this, or a roadmap? I'd be willing to help on this feature! I'm using Next.js right now with previewMode, could something like Dynamic imports be used?
Maybe we could open up a project for this.
You're absolutely right, dynamic imports is how this will be done. However, due to constraints with bundlers we're not able to put those dynamic imports inside the libraries defined in tinacms/tinacms
. Instead, the packages need to be constructed in such a way that the user can choose to dynamically import them in their site.
This isn't really a cohesive "feature", but just a property that some of these packages should have. The biggest effect will be to move the MarkdownFieldPlugin and HtmlFieldPlugin out of the tinacms
package and into the react-tinacms-editor
.
This will be a breaking change.
In order to get a wysiwyg in your site you will have to do the following:
import { TinaCMS } from "tinacms"
import { HtmlFieldPlugin } from "react-tinacms-editor"
const cms = new TinaCMS({ ... })
cms.plugins.add(HtmlFieldPlugin)
If you want it to split it out using dynamic imports then you would instead do.
import { TinaCMS } from "tinacms"
const cms = new TinaCMS({ ... })
import("react-tinacms-editor").then(({ HtmlFieldPlugin }) => {
cms.plugins.add(HtmlFieldPlugin)
})
aside: I'm not sure there's enough here to warrant a new GitHub project at the moment
cc: @jpuri
Yeah sounds great, for now what I'm doing is the following for NextJS, which makes for a considerably smaller build. In pages/_app
I'm dynamically importing a wrapper for TinaCMS like so
if (!pageProps.preview) {
return <Component {...pageProps} />;
}
const Tina = dynamic(
() => import("../components/Tina"),
{
ssr: false,
}
);
return (
<Tina>
<Component {...pageProps} />
</Tina>
);
./components/Tina
import { ReactNode } from "react";
import { Tina as BaseTina, TinaCMS } from "tinacms";
export default function Tina({ children }: { children: ReactNode }) {
//TODO add options passthrough to cms object
const cms = new TinaCMS();
return <BaseTina cms={cms}>{children}</BaseTina>;
}
And then on each page where I would need Tina I'm checking if preview mode is enabled if it is I load the hooks I need for Tina. I am currently using these(100kbish)
import { useForm, usePlugin } from "@tinacms/react-core";
as the standard were still giving me large bundle sizes (400kbish)
import { useForm, usePlugin } from "tinacms";
But I'm not sure if this is related.
This is a fantastic!
You've found exactly why useForm
is defined in @tinacms/react-core
instead of tinacms
.
Hey @ncphillips : an option could be that we have a wrapper over tinacms where we do these dynamic imports looking at the configurations passed by user. If user tells that they need markdown field we dynamically import editor.
This will keep tina core small.
It definitely does not require new repo.
Sorry for the confusion I didn't mean a new repo but an internal project like the WYSIWYM project https://github.com/tinacms/tinacms/projects. I don't know if it warrants it though.
Hey everyone, I've been looking at this today. Checkout the above PR if you want to see what's up.
warning: long post ahead, feel free to skip to "future improvements" heading to see what I think the API provided by Tina could look like.
I want to share the solutions that I've come up with for a Next.js site I'm working on. My goal was to have as little code as possible included from Tina in my production/static builds, and while my current solution is not yet very ergonomic, I think it could be developed further into something that is simple enough to be used by most/all Tina projects.
@ncphillips:
You're absolutely right, dynamic imports is how this will be done. However, due to constraints with bundlers we're not able to put those dynamic imports inside the libraries defined in tinacms/tinacms. Instead, the packages need to be constructed in such a way that the user can choose to dynamically import them in their site.
This is sort of the approach I followed, but I think that (generally speaking) it's OK to load a lot of heavy dependencies, if you only load them after editing is enabled at run time.
Note that I am showing this only because it's currently working for me, I outline at the end of this post how it could be developed into a more friendly/re-usable solution.
The main mechanism is a lib/tina
file that uses dynamic imports + a react context to lazy-load Tina at runtime. The rest of my app only accesses tina through the API exposed here.
// lib/tina/index.tsx
import { createContext, createElement, useContext, useState, useEffect } from 'react'
const NoopProvider: React.FC = ({ children }) => children as React.ReactElement
const EnabledContext = createContext(false)
export type { BlockItem, BlockType } from './blockTemplates'
export function useTinaEnabled() {
return useContext(EnabledContext)
}
export function loadTina() {
// the DO_NOT_IMPORT file exports a pre-configured "real" provider and re-exports all of the Tina hooks I use elsewhere
return import('./DO_NOT_IMPORT')
}
export default function DynamicTinaProvider({
enable = false,
children,
}: {
enable: boolean
children: React.ReactNode
}) {
const [{ Provider, enabled }, setState] = useState<{ Provider: React.FC<any>; enabled: boolean }>(
{
Provider: NoopProvider,
enabled: false,
},
)
useEffect(() => {
if (enable) {
loadTina().then(({ RealTinaProvider }) => {
setState({ Provider: RealTinaProvider, enabled: true })
})
} else {
setState((prev) => (prev.enabled ? prev : { Provider: NoopProvider, enabled: false }))
}
}, [enable])
return createElement(
EnabledContext.Provider,
{ value: enabled },
createElement(Provider, {}, children),
)
}
I use this provider in _app.tsx
like this:
export default function MyApp({ Component, pageProps }) {
React.useEffect(() => setTinaEnabled(process.env.NODE_ENV !== 'production'), [])
return (
<DynamicTinaProvider enable={tinaEnabled}>
<Component {...pageProps} />
</DynamicTinaProvider>
)
}
Finally, my actual pages look like this:
import {useTinaEnabled, loadTina} from 'lib/tina'
import dynamic from 'next/dynamic'
export function SomePage(props) {
const tinaEnabled = useTinaEnabled()
if (tinaEnabled) {
return <EditablePage {...props}/>
} else {
return <StaticPage {...props} />
}
}
const StaticPage = (props) => {
return <Layout><h1>Hello {props.data.name}</h1>
}
const EditablePage = dynamic(() => {
return loadTina().then(tina => {
return function EditablePage(props) {
const [modifiedData] = tina.useLocalForm(...)
return <StaticPage {...props} data={modifiedData}/>
}
},
{ ssg: false }
}
Obviously that's quite a bit of extra work, but it has the advantage of using only ES7 features, so most of the boilerplate could be abstracted into component decorators that are exported from Tina libraries.
As pointed out above, it's probably not a good idea to use dynamic import(...)
syntax inside a library, so at some point the user needs to write that, but a modified version of the DynamicTinaProvider
I wrote above could accept the loadTina
callback as a prop allowing users to write this:
// inside the users application
import { TinaProvider } from 'tinacms/dynamic'
function loadTina() {
return import('tinacms')
}
export function App() {
return <TinaProvider loadTina={loadTina}>
... my app ...
</TinaProvider>
}
I think that's a reasonable trade-off in terms of ergonomics for the provider.
Most of the per-page boilerplate I wrote above could be abstracted into a single component decorator. The strawman API I would put forward is something like editable(editProps, component)
that could be used like this:
import {editable} from 'next-tinacms-editable'
import {loadTina} from '../lib/tina'
const Page = editable(loadTina, editProps, function StaticPage(props) {
return <Layout><h1>Hello {props.data.name}</h1></Layout>
})
// this only gets called if tina is enabled via context
function editProps(pageProps, tina) {
const [modifiedData] = tina.useForm(...) // set up a form using pageProps.data
return {...pageProps, data: modifiedData}
}
export default Page
In this case I think it makes sense to export the editable
decorator from a framework-specific package, since different frameworks might have different preferences for how to load dynamic components. However, the 'tinacms/dynamic' module could export a version that uses React suspense. Something like this: (warning, not tested at all!)
// in tinacms/dynamic
export function editable(loadTina, editProps, Static) {
const LazyEditable = React.lazy(() => loadTina().then(tina => {
return function Editable(props) {
const modifiedProps = editProps(props, tina)
return <Static {...modifiedProps}/>
}
})
return function Dynamic(props) {
const tinaEnabled = useContext(EnabledContext)
if (tinaEnabled) {
return <React.Suspense fallback={<Static {...props}/>}>
<LazyEditable {...props}/>
</React.Suspense>
} else {
return <Static {...props}/>
}
}
}
Edit: fixed some formatting
An update: I also found I wanted something like this for inline editing, where it makes sense to replace the entire element instead of just updating it's props.
export function editableInline<P>(
StaticComponent: ComponentType<P>,
EditableComponent: ComponentType<P & { tina: typeof import('./DO_NOT_IMPORT') }>,
) {
const LazyEditable = dynamic<P>(
() =>
loadTina().then((tina) => (props) => createElement(EditableComponent, { ...props, tina })),
{ ssr: false },
)
return function DynamicallyEditable(props: P) {
const state = useContext(TinaStateContext)
if (state === 'loaded') {
return createElement(LazyEditable, props)
} else {
return createElement(StaticComponent, props)
}
}
}
(this is using next/dynamic
but could instead use React.Suspense like the previous example code.
We also made a specific version for InlineWysiwyg
in the tinacms.org website. This version only loads if you're in edit mode, so that JS will never be requested for regular users of your website.
import { useInlineForm } from 'react-tinacms-inline'
import React from 'react'
export function InlineWysiwyg(props: any) {
const { status } = useInlineForm()
const [{ InlineWysiwyg }, setEditor] = React.useState<any>({})
React.useEffect(() => {
if (!InlineWysiwyg && status === 'active') {
import('react-tinacms-editor').then(setEditor)
}
}, [status])
if (InlineWysiwyg) {
return <InlineWysiwyg {...props} />
}
return props.children
}
You can just use it as you would use the InlineWysiwyg
normally.
Hello, I started using tinaCms recently and just realized that it more than doubled my bundle size :sweat_smile:
I was wondering if it would be a way to approach this like some isomorphic libs do:
// index.js
exports.module = process.env.PASSTROUGH ? require('inline-textarea-passtrough') : require('inline-textarea');
where the passtrough is essentially what's currently the else-case of if (cms.enabled) {
conditions.
I feel like i'm currently kinda abusing tinaCMS as I use it for inline editing my page, but only in dev mode - on production i don't provide functionality to enter edit mode, so having anything "tina" in the output is especially sad. Would sth like that make sense and be in the scope of tina?
I'm taking another look at this today.
The inline editing packages are not the problem here. I ran @next/bundle-analyzer
on the tinacms.org website to see what kind of effect Tina was having on that site. Here are the results:
The fact is that TinaCMS code is not very big so it's not the problem.
These are the culprits:
Size | Package | Purpose |
---|---|---|
575kb | `moment | Used in the DateFieldPlugin |
230kb | react-beautiful-dnd |
Used by Blocks and Group Lists (inline and sidebar) |
160kb | react-color |
Used by the ColorFieldPlugin |
moment
is definitely the easiest package to deal with. You can drastically reduce it's impact by using the moment-locales-webpack-plugin
which will strip out a lot of unnecessary information. On tinacms.org adding this plugin reduced moment's impact on bundle size from 575kb to 147kb
To improve things further the DateFieldPlugin
and the ColorFieldPlugin
may be extracted from tinacms
in the same way that the wysiwyg was. By doing this we will make it possible to dynamically load those plugins so moment
, react-datetime
, and react-color
do not impact your initial bundle.
Since react-beautiful-dnd
is used by Blocks and Groups both inline and in forms it will be a bit more awkward to deal with. I'll think on this some more.
Still room for improvement but adding moment-locales-webpack-plugin
and extracting those two plugins should reduce the TinaCMS bundle size impact by 965kb.
Hey @ncphillips, while I'm glad to see the work going in to minimizing Tinas the default bundle size, I have a couple of questions:
My perception is that for most static/JAMstack sites, there is a strong distinction between (and distinct trade-offs for) editors and readers. In the case of readers, excluding as much of Tina + deps as possible is very desirable. In the case of editors, they benefit more from caching and it's not really a problem to have some extra dependencies.
I'm (mostly) fine with the workarounds I'm using to lazy-load Tina in my production builds, but I wonder if I'm using Tina in a way that's at odds with the goals of the project.
@grncdr I think TinaCMS by default should be lazy loaded when the user begins editing content. Will make the dev UX way better and make the website more better for everyone including those who contribute/edit content and visitors.
Tina payload should even be 0 for regular visitors, it's possible to load Tina UI only from a custom route like admin
Tina payload should even be 0 for regular visitors, it's possible to load Tina UI only from a custom route like
admin
That might seem like a better idea now that I thought about more about the components that interface on the DOM.
Maybe it's possible to LazyLoad the TinaCMS components for a zero-config developer experience by checking whether the user has requested for the component(edit state) and then fetch the JS files and then do a force update of components that have the TinaCMS components implemented.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
@ncphillips @jeffsee55 I'm not so sure this should have been closed. Here's deployment sizes before Tina:
And here's my deployment sizes after Tina:
That's a ~500kb difference for every page that uses the useTina()
hook. What gives? Is there a different way I should be going this? I just followed the instructions on the main docs after reading them from beginning to end a good 2 or 3 times. I guess I'm kind of surprised at how stark of a difference I'm seeing now. Is there anything that can be done here? I thought the benefit of using Tina was that we're able to statically build these pages before hand, therefore there shouldn't be any increase (or at least a negligible one at worst).
@cereallarceny this definitely shouldn't be happening. We'll take a look! Any additional info on your setup would help. Are you certain the increase in bundle is due to useTina
or could it be due to something in _app.js
because it looks like that one has increased too.
Well, I originally had a <Layout />
component inside my _app.tsx
file, which simply renders the appropriate page meta tags, the top navbar, and the footer. However, because these components rely on a content/common/index.json
file that's editable in Tina, and thus require the use of getStaticProps
, I had to move it out of my _app.tsx
and into each of the files in the /pages
folder. This is less than ideal, but was the simplest fix I could fashion.
_app.tsx
import type { AppProps } from 'next/app';
import { TinaProvider } from '../.tina';
import '../assets/fonts/recoleta/stylesheet.css';
const Raise: React.FC<AppProps> = ({ Component, pageProps }) => {
if (pageProps?.statusCode === 404 || pageProps?.statusCode === 500) {
// @ts-ignore
return <Component {...pageProps} />;
}
return (
<TinaProvider>
<Component {...pageProps} />
</TinaProvider>
);
};
export default Raise;
And here's one of the pages, but they all basically look the same:
pages/index.tsx
import React from 'react';
import { useTina } from 'tinacms/dist/edit-state';
import type { NextPage } from 'next';
import Components from '../components/pages/index';
import Layout from '../components/Layout';
import { client } from '../.tina';
import type { TinaPageGetStaticProps, PageHomepage } from '../.tina';
const Homepage: NextPage<TinaPageGetStaticProps<PageHomepage>> = ({
common,
page,
}) => {
const { data } = useTina(page);
// TODO: Not sure why we have to perform this hack on every page, but we do...
if (!data?.page || data.page._sys.basename !== 'homepage.json') return null;
return (
<Layout {...common.data.common}>
<Components {...data.page} />
</Layout>
);
};
export const getStaticProps = async () => ({
props: {
common: await client.queries.common({ relativePath: 'index.json' }),
page: await client.queries.page({ relativePath: 'homepage.json' }),
},
});
export default Homepage;
Can you share the contents of TinaProvider
?
Oh, TinaProvider
just exports the TinaDynamicProvider
. My .tina/index.ts
file looks like this:
import type { Common } from './__generated__/types';
export { default as schema, config } from './schema';
export { default as TinaProvider } from './components/TinaDynamicProvider';
export { client } from './__generated__/client';
export type TinaPageGetStaticProps<T extends any> = {
common: { data: { common: Common }; query: string; variables: object };
page: { data: { page: T }; query: string; variables: object };
};
export * from './__generated__/types';
TinaDynamicProvider.tsx
looks like this (auto-generated/grabbed directly from the docs):
import dynamic from 'next/dynamic';
import { TinaEditProvider } from 'tinacms/dist/edit-state';
const TinaProvider = dynamic(() => import('./TinaProvider'), { ssr: false });
const DynamicTina = ({ children }) => (
<TinaEditProvider editMode={<TinaProvider>{children}</TinaProvider>}>
{children}
</TinaEditProvider>
);
export default DynamicTina;
I've confirmed this is not an issue in our starter
Page Size First Load JS
┌ /_app 0 B 87.3 kB
├ ● /[filename] (759 ms) 4.06 kB 118 kB
├ ├ /about (420 ms)
├ └ /home (339 ms)
├ ○ /404 2.51 kB 117 kB
├ ○ /admin (521 ms) 517 kB 604 kB
├ ● /post/[filename] (873 ms) 7.98 kB 122 kB
├ ├ /post/anotherPost (450 ms)
├ └ /post/voteForPedro (423 ms)
└ ● /posts (425 ms) 1.5 kB 116 kB
+ First Load JS shared by all 87.3 kB
├ chunks/framework-7f78491ac389bdeb.js 46.5 kB
├ chunks/main-922238a218db66d1.js 34.8 kB
├ chunks/pages/_app-4effbb51b7a86029.js 3.75 kB
├ chunks/webpack-fe7918c2447839a4.js 2.27 kB
└ css/23fa6976e7f42ea7.css 7.73 kB
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
One thing you can try to confirm this with is remove Tina stuff from the _app.js
, you'll want to also remove calls to useTina
but things should work exactly the same without Tina.
Hey @jeffsee55 , I looked into what you said and realized the problem immediately. I was exporting a dynamically imported component. The _app.tsx
is using that "re-exported" component <TinaProvider />
. Funny enough, and I've never run into this in my life, doing this re-exporting essentially erased the dynamic import nature of that original <TinaProvider />
component. Once I just imported it directly, like such:
import type { AppProps } from 'next/app';
// This is the important line (compare to what I posted above)
import TinaProvider from '../.tina/components/TinaDynamicProvider';
import '../assets/fonts/recoleta/stylesheet.css';
const Raise: React.FC<AppProps> = ({ Component, pageProps }) => {
if (pageProps?.statusCode === 404 || pageProps?.statusCode === 500) {
// @ts-ignore
return <Component {...pageProps} />;
}
return (
<TinaProvider>
<Component {...pageProps} />
</TinaProvider>
);
};
export default Raise;
... I see the following change in build sizes:
I'd call that fairly significant and these result bring me back to my pre-Tina build sizes. Now I just need to figure out why my build is so large without Tina! 😆
We can probably close this issue at this point.
Ah wow! That's very strange. I stared at your example code for a while and it seemed fine 😆
Summary
I've been busy with getting a Gatsby site into production and I observed that the app bundle size became unusually large (1.7MB uncompressed), so I installed webpack bundle analyzer and generated a report. And it turned out, that TinaCMS is the main culprit, accounting for around 800-900KB of the size, including dependencies, which are irrelevant to the production build (but are needed for the development server with Tina of course).
I take guidance from the excellent articles The Cost of JavaScript 2018 and The Cost of JavaScript 2019. The overhead that stems from TinaCMS is almost as large as the median uncompressed bundle size. This is a huge obstacle for using Tina with production sites.
Basic example
TinaCMS + its dependencies should not be present in production build. I have no idea though how this can be achieved, since Tina is integrated with code of the site. First thing that comes to mind is Webpack hacking. Maybe you already know that this is a problem and already have an idea how to fix this? If so, this should be part of the docs, since out-of-the-box this blow-up happens.
Here is the report of bundle analyzer of my production build
And this is a regex to filter for dependencies, that are exclusively needed by Tina
(@tinacms|prosemirror|codemirror|react-datetime|react-beautiful-dnd|markdown-it|final-form|styled-components|react-dropzone|react-color|tinycolor2|stylis).*.js
Motivation
Obviously faster page loads and less production traffic. Tina should not be that expensive.