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

Delay loading TinyMCE until a classic block is edited #21738

Closed
sarayourfriend opened this issue Apr 20, 2020 · 6 comments
Closed

Delay loading TinyMCE until a classic block is edited #21738

sarayourfriend opened this issue Apr 20, 2020 · 6 comments
Labels
[Block] Classic Affects the Classic Editor Block [Status] In Progress Tracking issues with work in progress [Type] Performance Related to performance efforts

Comments

@sarayourfriend
Copy link
Contributor

sarayourfriend commented Apr 20, 2020

The problem

TinyMCE is a huge dependency that is used rarely in the context of the block editor. Primarily it is used to support the classic block and some meta boxes. In total (TinyMCE core and plugins) it costs 272.274KB in compressed trasferred JS and 1,007.209KB parsed (that's over a MB in parse!). This is a big cost to pay for something that many users will never interact with.

A solution

The solution I'd like to explore is to offset loading TinyMCE as often as possible until it is needed. Given certain edge cases and exclusions, this would offset the cost of loading TinyMCE except in the following case, which are the minority of use cases:

  1. We cannot do this when non-backwards compat meta boxes are installed as they may depend on TinyMCE (as in the case of Advanced Custom Fields, for example).
  2. We cannot do this when a classic block is already being used on a post (for reasons described in this issue Automatically and transparently load block assets asynchronously #23098 in the "Problems with blocks already used on a post" section).

Otherwise, when the classic block has not already been added to a post or when custom meta boxes aren't installed, we can offset the TinyMCE dependency until the block's edit is run. Indeed, in this case, if someone never uses the classic block, they will not pay the penalty for having it.

There was an existing draft PR for asynchronously loading TinyMCE that built upon an existing PR in Gutenberg that introduced a REST API for retrieving a list of dependencies. This REST API isn't technically necessary for asynchronously loading TinyMCE for the classic block, it was included in the draft PR as a way of looking forward to enabling similar async behavior in other blocks.

However, the REST API itself presents some complexities and concerns:

  1. REST calls to WP are expensive as all of WP needs to spin up, and in the current implementation it could require more than one call if someone ran with the experimental version.
  2. While the REST API does return inline scripts, TinyMCE’s inline scripts are complex and varied (there are three, not all are "registered" in WP_Scripts).

Because we can get the URL for TinyMCE without using a REST API (partially it's already available in the tinyMCEPreInit object but this won't be sufficient as I describe below) I think we can shortcut getting a win against TinyMCE around the REST API.

To accomplish this async TinyMCE project, I propose the following:

  1. Creat a LazyLoadTinyMCE component to wrap the ClassicEdit component.
    • This LazyLoadTinyMCE component is basically a TinyMCE specific version of the the solution described in this PR.
  2. Cutting TinyMCE from the initial pageload of the block editor whenever possible.

TinyMCE URLs

There is one caveat about using tinyMCEPreInit.baseURL is that we need to decide which TinyMCE script to use as different scripts are used for different environments and compression settings. I’m not totally sure how to get this information to the frontend. One potential solution is to use $wp_scripts->registered['wp-tinymce']->deps and then follow a strategy similar to get_url in the REST API PR to retrieve the URI for the wp-tinymce scripts and then add those to an array the frontend can use. Adding functionality like get_url to WP_Scripts directly would be good for making the functionality available generally. It could alternatively be added as a utility function in Gutenberg's plugin, but in any case, it would need to be available outside the REST API.

Stopping WordPress from equeueing TinyMCE

Along with that, we also need to be enable Gutenberg to prevent core from enqueuing the TinyMCE scripts. To do that we ought to move the printing of the editor scripts into an action that can be overwritten by the plugin. This trac ticket proposes that change: https://core.trac.wordpress.org/ticket/49964

Edge cases/exclusions

Meta boxes

As stated above, there are some exceptions to when we can do this. Meta boxes present a backwards compatability issue as some of them depend directly on TinyMCE. Meta box development hasn’t stopped and widely used plugins like Advanced Custom Fields depend on them. Preserving the ability for meta boxes that depend on TinyMCE to continue to work is part of the work for v1. We’ll do this by disabling async TinyMCE when we detect that custom meta boxes are being used.

From what I understand at the moment edit-form-blocks.php enqueues the editor scripts before processing metabox data. Ideally we would like to have already run through processing meta boxes before we render scripts for the editor so that we have some basis for deciding whether meta boxes are really being used. register_and_do_post_meta_boxes takes care of processing meta boxes for a post. I think we can move this logic before the call to wp_enqueue_editor in edit-form-blocks.php without consequence and then look into the global $wp_meta_boxes in the action we'll add in Gutenberg to override TinyMCE script printing.

When the classic block is already used on a post

When a post is first loaded into the block editor, the edit for each block gets run. This means that when a classic block exists on a post, TinyMCE will be needed immediately on page load. Asynchronously loading TinyMCE here isn't possible because we need to render the block's contents into the editor. I think we should solve this by cutting this from the scope of the async TinyMCE project and instead solve it as part of the wider project to async all block dependencies whenever possible (if indeed it is a solvable problem).

We'll need to introspect into a posts post_content and decide whether we think the classic block is being used. There already exists a function has_blocks, however it relies on the block's boundaries being renderedinto the post_content and unfortunately, the classic block doesn't render block markup comments. So the only way I can think of right now to do it is to run the post through parse_blocks and then walk the block tree and see if a core/freeform block exists.

Summary of changes

To summarize, we need to make changes to WordPress core and Gutenberg.

  • Core
    1. Devise a way to detect whether meta boxes that use TinyMCE are present that would be usable the Gutenberg plugin so that it can decide when to follow the default path of eagerly loading TinyMCE.
    2. Start injecting the TinyMCE dependency URIs into the frontend.
    3. Move printing editor scripts into an action overridable by the Gutenberg plugin so that we can prevent TinyMCE scripts from being injected when meta boxes are not present and when the classic block is not already being used for the post being edited.
  • Gutenberg
    1. Create a version of the LazyLoad component that is able to load dependencies from URIs and that does not use the REST API.
    2. Prevent TinyMCE from being loaded by overriding the action we created in core. Also print translation initialization into a JS function to be executed by LazyLoadTinyMCE once the dependency is loaded.
    3. Wrap the classic block’s edit component with LazyLoadTinyMCE.

Completing the above will deliver a significant decrease in JavaScript download and parse times for most Gutenberg users.

Alternatives

We could move TinyMCE into the block directory, however then every post would pay the cost of the dependency, regardless of whether it was going to be used, so I don't think this is an adequate solution.

Alternatively, we could move it into the block directory and then have the block directory offset loading a block's dependencies until it is edited... but that's just the other project documented in #23098.

@gziolo gziolo added [Type] Performance Related to performance efforts Needs Technical Feedback Needs testing from a developer perspective. [Block] Classic Affects the Classic Editor Block labels Apr 21, 2020
@aduth
Copy link
Member

aduth commented Apr 28, 2020

I'm reluctant to add complexity to an already-difficult problem which has lost momentum many times in the past, but I have two observations where I think we may orient the effort to set us on the right foot without making incremental progress more difficult:

  1. I really think we shouldn't focus so much on TinyMCE specifically here, but instead toward a more general problem of on-demand block loading (including dependencies). In this case, it's largely a problem of: TinyMCE is only needed to be loaded for the Classic block, and in a lot of cases the Classic block isn't ever present in a post (increasingly true as time progresses). To put the focus on how to asynchronously load blocks will establish a much more generalized (reusable!) solution to this problem.
    • Example: In earlier iterations of the block editor, the Code block included syntax highlighting using Prism.js. While certainly a usability improvement, it was inevitably reverted because the dependency, like TinyMCE, was too large to justify loading, especially when many users would not be using the Code block.
    • Aside: I sense this may be the underlying goal, but I observe that both the wording here and the implementation in Asynchronously load tinyMCE the first time a classic block is edited #21684 is heavily targeted at TinyMCE specifically. I'd be disappointed if this turned resulted in an ad hoc solution that couldn't easily be applied to other blocks.
  2. Maybe TinyMCE is too difficult to tackle in a first iteration? Notably, there's a lot of complexity in how it's initialized (example code). Relative to the first point, while implementing a generalized solution for blocks might involve more work, we could offset this by focusing on simpler scripts first. A counterpoint could be made that it would be wise to have a good understanding of the complexities in something like TinyMCE. It's true, though very specific to this script, and I'd be disappointed if focusing so much on TinyMCE would either stall progress or put us into a position where we're forced to tailor a solution which excels at supporting a very specific (and non-generalizable) set of requirements.

I guess it depends how much we consider this to be low-hanging fruit. TinyMCE is large and there's a lot of savings to be had by loading it asynchronously. On the other hand, I typically view low-hanging fruit as being "easy". To me, the work around loading TinyMCE is far from easy 😅

In general, I like the direction that #21684 is going so far as having a component which can simply receive a script handle and resolve/load it and all of its dependencies. I think the difference for me would be to optimize toward this being used by the framework, to pass a block type's registered script handle setting, rather than by the Classic block to pass 'tinymce'.

@gziolo
Copy link
Member

gziolo commented Apr 29, 2020

In general, I like the direction that #21684 is going so far as having a component which can simply receive a script handle and resolve/load it and all of its dependencies. I think the difference for me would be to optimize toward this being used by the framework, to pass a block type's registered script handle setting, rather than by the Classic block to pass 'tinymce'.

Yes, it would be ideal to have a solution like this. It's already complex enough because it needs to account for the fact that such script can have other dependencies and some of them might be already injected in the page. One thing that isn't clear for me here is whether it means that you would see the Classic block being lazy-loaded or it's edit implementation?

Have you considered extracting Classic block to its own npm package/WordPress module and make it exposed server-side? It would make it possible to remove the script or even unregister the block at all on pages that don't use Classic block. It might be a viable solution for many sites that don't need the legacy handling provided by the Classic block and it's quite fast to implement.

@sarayourfriend
Copy link
Contributor Author

It's already complex enough because it needs to account for the fact that such script can have other dependencies and some of them might be already injected in the page

Thanks for bringing that up. We could add getting the list of already loaded scripts and styles to the client along with the preloaded list of known dependency URIs (as part of the v0.2 I proposed in the PR: #21684 (comment)).

Have you considered extracting Classic block to its own npm package/WordPress module and make it exposed server-side?

I had not considered that. I figure that it would indeed work as long as we coupled the work to prevent TinyMCE from being put into the page... but it would be a big workflow change for people who frequently use the classic block, no?

@gziolo
Copy link
Member

gziolo commented Apr 30, 2020

I figure that it would indeed work as long as we coupled the work to prevent TinyMCE from being put into the page... but it would be a big workflow change for people who frequently use the classic block, no?

I don’t think it’s something that WordPress core would enforce on users. It’s rather an option for site owners to opt out from the Classic block with one line of code or admin setting. Eventually Classic block could install on demand by becoming it’s own plugin that is featured in Block Directory that is planned for later this year.

@sarayourfriend
Copy link
Contributor Author

sarayourfriend commented Jun 11, 2020

Okay y'all, I think I've come up with a new version of this plan. I've updated the issue description to reflect it (feel free to check out the earlier revisions for context and changes). I'm also linking this to a master thread about asyncing block dependencies generally: #2768

@gziolo gziolo added [Status] In Progress Tracking issues with work in progress and removed Needs Technical Feedback Needs testing from a developer perspective. labels Jun 13, 2020
@sarayourfriend
Copy link
Contributor Author

Closing in favor of continuing implementation detail discussion in #23068 and #2768

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Block] Classic Affects the Classic Editor Block [Status] In Progress Tracking issues with work in progress [Type] Performance Related to performance efforts
Projects
None yet
Development

No branches or pull requests

3 participants