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

Lodash: Refactor away from _.omit() in style addSaveProps() #45014

Merged
merged 5 commits into from
Oct 19, 2022

Conversation

tyxla
Copy link
Member

@tyxla tyxla commented Oct 17, 2022

What?

This PR removes Lodash's _.omit() from the style addSaveProps hook in favor of a custom alternative utility function

Why?

Lodash is known to unnecessarily inflate the bundle size of packages, and in most cases, it can be replaced with native language functionality. See these for more information and rationale:

This is technically the most complex remaining use of _.omit() and I believe that the way Lodash handles arguments in a different way can very easily be concealed, which could lead to subtle bugs.

How?

We're creating a custom utility function that handles the omission of styles recursively and still supports all use cases that the Lodash function used to. We're adding thorough documentation and unit tests to ensure that the current way it works is more transparent than it was.

Testing Instructions

@tyxla tyxla added [Type] Performance Related to performance efforts [Type] Code Quality Issues or PRs that relate to code quality [Package] Block editor /packages/block-editor labels Oct 17, 2022
@tyxla tyxla requested a review from ellatrix as a code owner October 17, 2022 11:20
@tyxla tyxla self-assigned this Oct 17, 2022
@tyxla tyxla force-pushed the refactor/add-save-props-omit branch from fd484d1 to 9589ffe Compare October 17, 2022 11:32
@github-actions
Copy link

github-actions bot commented Oct 17, 2022

Size Change: +1.45 kB (0%)

Total Size: 1.27 MB

Filename Size Change
build/block-editor/index.min.js 168 kB +636 B (0%)
build/block-editor/style-rtl.css 15.8 kB +279 B (+2%)
build/block-editor/style.css 15.8 kB +283 B (+2%)
build/block-library/blocks/image/editor-rtl.css 880 B -4 B (0%)
build/block-library/blocks/image/editor.css 880 B -2 B (0%)
build/block-library/editor-rtl.css 11.2 kB +2 B (0%)
build/block-library/editor.css 11.2 kB +2 B (0%)
build/block-library/index.min.js 192 kB +206 B (0%)
build/components/index.min.js 202 kB -2 B (0%)
build/edit-post/index.min.js 31.7 kB -26 B (0%)
build/edit-site/index.min.js 57.8 kB +68 B (0%)
build/editor/index.min.js 41.7 kB +14 B (0%)
build/editor/style-rtl.css 3.62 kB -5 B (0%)
build/editor/style.css 3.61 kB -3 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 982 B
build/annotations/index.min.js 2.76 kB
build/api-fetch/index.min.js 2.26 kB
build/autop/index.min.js 2.14 kB
build/blob/index.min.js 475 B
build/block-directory/index.min.js 7.09 kB
build/block-directory/style-rtl.css 990 B
build/block-directory/style.css 991 B
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 90 B
build/block-library/blocks/archives/style.css 90 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 122 B
build/block-library/blocks/audio/style.css 122 B
build/block-library/blocks/audio/theme-rtl.css 126 B
build/block-library/blocks/audio/theme.css 126 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 84 B
build/block-library/blocks/avatar/style.css 84 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 482 B
build/block-library/blocks/button/editor.css 482 B
build/block-library/blocks/button/style-rtl.css 523 B
build/block-library/blocks/button/style.css 523 B
build/block-library/blocks/buttons/editor-rtl.css 337 B
build/block-library/blocks/buttons/editor.css 337 B
build/block-library/blocks/buttons/style-rtl.css 332 B
build/block-library/blocks/buttons/style.css 332 B
build/block-library/blocks/calendar/style-rtl.css 239 B
build/block-library/blocks/calendar/style.css 239 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 100 B
build/block-library/blocks/categories/style.css 100 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 121 B
build/block-library/blocks/code/style.css 121 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 187 B
build/block-library/blocks/comment-template/style.css 185 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 834 B
build/block-library/blocks/comments/editor.css 832 B
build/block-library/blocks/comments/style-rtl.css 632 B
build/block-library/blocks/comments/style.css 630 B
build/block-library/blocks/cover/editor-rtl.css 612 B
build/block-library/blocks/cover/editor.css 613 B
build/block-library/blocks/cover/style-rtl.css 1.57 kB
build/block-library/blocks/cover/style.css 1.55 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 410 B
build/block-library/blocks/embed/style.css 410 B
build/block-library/blocks/embed/theme-rtl.css 126 B
build/block-library/blocks/embed/theme.css 126 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 253 B
build/block-library/blocks/file/style.css 254 B
build/block-library/blocks/file/view.min.js 346 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 948 B
build/block-library/blocks/gallery/editor.css 950 B
build/block-library/blocks/gallery/style-rtl.css 1.53 kB
build/block-library/blocks/gallery/style.css 1.53 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 394 B
build/block-library/blocks/group/editor.css 394 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 327 B
build/block-library/blocks/html/editor.css 329 B
build/block-library/blocks/image/style-rtl.css 627 B
build/block-library/blocks/image/style.css 630 B
build/block-library/blocks/image/theme-rtl.css 126 B
build/block-library/blocks/image/theme.css 126 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 213 B
build/block-library/blocks/latest-posts/editor.css 212 B
build/block-library/blocks/latest-posts/style-rtl.css 463 B
build/block-library/blocks/latest-posts/style.css 462 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 507 B
build/block-library/blocks/media-text/style.css 505 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 705 B
build/block-library/blocks/navigation-link/editor.css 703 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 296 B
build/block-library/blocks/navigation-submenu/editor.css 295 B
build/block-library/blocks/navigation/editor-rtl.css 2.02 kB
build/block-library/blocks/navigation/editor.css 2.03 kB
build/block-library/blocks/navigation/style-rtl.css 2.17 kB
build/block-library/blocks/navigation/style.css 2.16 kB
build/block-library/blocks/navigation/view-modal.min.js 2.78 kB
build/block-library/blocks/navigation/view.min.js 443 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 174 B
build/block-library/blocks/paragraph/editor.css 174 B
build/block-library/blocks/paragraph/style-rtl.css 279 B
build/block-library/blocks/paragraph/style.css 281 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 493 B
build/block-library/blocks/post-comments-form/style.css 493 B
build/block-library/blocks/post-date/style-rtl.css 61 B
build/block-library/blocks/post-date/style.css 61 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 586 B
build/block-library/blocks/post-featured-image/editor.css 584 B
build/block-library/blocks/post-featured-image/style-rtl.css 315 B
build/block-library/blocks/post-featured-image/style.css 315 B
build/block-library/blocks/post-navigation-link/style-rtl.css 153 B
build/block-library/blocks/post-navigation-link/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 282 B
build/block-library/blocks/post-template/style.css 282 B
build/block-library/blocks/post-terms/style-rtl.css 96 B
build/block-library/blocks/post-terms/style.css 96 B
build/block-library/blocks/post-title/style-rtl.css 100 B
build/block-library/blocks/post-title/style.css 100 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 135 B
build/block-library/blocks/pullquote/editor.css 135 B
build/block-library/blocks/pullquote/style-rtl.css 326 B
build/block-library/blocks/pullquote/style.css 325 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 282 B
build/block-library/blocks/query-pagination/style.css 278 B
build/block-library/blocks/query-title/style-rtl.css 63 B
build/block-library/blocks/query-title/style.css 63 B
build/block-library/blocks/query/editor-rtl.css 439 B
build/block-library/blocks/query/editor.css 439 B
build/block-library/blocks/quote/style-rtl.css 213 B
build/block-library/blocks/quote/style.css 213 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 409 B
build/block-library/blocks/search/style.css 406 B
build/block-library/blocks/search/theme-rtl.css 114 B
build/block-library/blocks/search/theme.css 114 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 234 B
build/block-library/blocks/separator/style.css 234 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 464 B
build/block-library/blocks/shortcode/editor.css 464 B
build/block-library/blocks/site-logo/editor-rtl.css 490 B
build/block-library/blocks/site-logo/editor.css 490 B
build/block-library/blocks/site-logo/style-rtl.css 203 B
build/block-library/blocks/site-logo/style.css 203 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 116 B
build/block-library/blocks/site-title/editor.css 116 B
build/block-library/blocks/site-title/style-rtl.css 57 B
build/block-library/blocks/site-title/style.css 57 B
build/block-library/blocks/social-link/editor-rtl.css 184 B
build/block-library/blocks/social-link/editor.css 184 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.4 kB
build/block-library/blocks/social-links/style.css 1.39 kB
build/block-library/blocks/spacer/editor-rtl.css 322 B
build/block-library/blocks/spacer/editor.css 322 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 494 B
build/block-library/blocks/table/editor.css 494 B
build/block-library/blocks/table/style-rtl.css 611 B
build/block-library/blocks/table/style.css 609 B
build/block-library/blocks/table/theme-rtl.css 190 B
build/block-library/blocks/table/theme.css 190 B
build/block-library/blocks/tag-cloud/style-rtl.css 251 B
build/block-library/blocks/tag-cloud/style.css 253 B
build/block-library/blocks/template-part/editor-rtl.css 235 B
build/block-library/blocks/template-part/editor.css 235 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 691 B
build/block-library/blocks/video/editor.css 694 B
build/block-library/blocks/video/style-rtl.css 174 B
build/block-library/blocks/video/style.css 174 B
build/block-library/blocks/video/theme-rtl.css 126 B
build/block-library/blocks/video/theme.css 126 B
build/block-library/classic-rtl.css 162 B
build/block-library/classic.css 162 B
build/block-library/common-rtl.css 1.02 kB
build/block-library/common.css 1.02 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 12.3 kB
build/block-library/style.css 12.3 kB
build/block-library/theme-rtl.css 719 B
build/block-library/theme.css 722 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/blocks/index.min.js 49.8 kB
build/components/style-rtl.css 11.3 kB
build/components/style.css 11.3 kB
build/compose/index.min.js 12.2 kB
build/core-data/index.min.js 15.5 kB
build/customize-widgets/index.min.js 11.3 kB
build/customize-widgets/style-rtl.css 1.38 kB
build/customize-widgets/style.css 1.38 kB
build/data-controls/index.min.js 653 B
build/data/index.min.js 8.08 kB
build/date/index.min.js 32.1 kB
build/deprecated/index.min.js 507 B
build/dom-ready/index.min.js 324 B
build/dom/index.min.js 4.7 kB
build/edit-navigation/index.min.js 16.1 kB
build/edit-navigation/style-rtl.css 3.99 kB
build/edit-navigation/style.css 4 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/style-rtl.css 7.13 kB
build/edit-post/style.css 7.13 kB
build/edit-site/style-rtl.css 8.37 kB
build/edit-site/style.css 8.35 kB
build/edit-widgets/index.min.js 16.7 kB
build/edit-widgets/style-rtl.css 4.34 kB
build/edit-widgets/style.css 4.34 kB
build/element/index.min.js 4.68 kB
build/escape-html/index.min.js 537 B
build/experiments/index.min.js 868 B
build/format-library/index.min.js 6.95 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.64 kB
build/html-entities/index.min.js 448 B
build/i18n/index.min.js 3.77 kB
build/is-shallow-equal/index.min.js 527 B
build/keyboard-shortcuts/index.min.js 1.78 kB
build/keycodes/index.min.js 1.83 kB
build/list-reusable-blocks/index.min.js 2.13 kB
build/list-reusable-blocks/style-rtl.css 835 B
build/list-reusable-blocks/style.css 835 B
build/media-utils/index.min.js 2.93 kB
build/notices/index.min.js 963 B
build/nux/index.min.js 2.06 kB
build/nux/style-rtl.css 732 B
build/nux/style.css 728 B
build/plugins/index.min.js 1.94 kB
build/preferences-persistence/index.min.js 2.22 kB
build/preferences/index.min.js 1.33 kB
build/primitives/index.min.js 933 B
build/priority-queue/index.min.js 1.58 kB
build/react-i18n/index.min.js 696 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.74 kB
build/reusable-blocks/index.min.js 2.21 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 10.6 kB
build/server-side-render/index.min.js 1.77 kB
build/shortcode/index.min.js 1.53 kB
build/style-engine/index.min.js 1.46 kB
build/token-list/index.min.js 644 B
build/url/index.min.js 3.61 kB
build/vendors/inert-polyfill.min.js 2.48 kB
build/vendors/react-dom.min.js 38.5 kB
build/vendors/react.min.js 4.34 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 268 B
build/widgets/index.min.js 7.21 kB
build/widgets/style-rtl.css 1.18 kB
build/widgets/style.css 1.19 kB
build/wordcount/index.min.js 1.06 kB

compressed-size-action

@youknowriad
Copy link
Contributor

I have to admit that I don't understand the reasoning for this PR. Why change a util that is already loaded and always will be (lodash is loaded as a separate script in WP) with a custom utility that we'd have to maintain.

  • We're adding more maintenance work for us
  • We're increasing the bundle size loaded in WP.

* @see https://lodash.com/docs/4.17.15#omit
*
* @param {Object} style Styles object.
* @param {Array|string} paths Paths to remove.
Copy link
Member

Choose a reason for hiding this comment

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

While creating a brand new omitStyle function, can be limit the paths argument to always be either one path, or always an array of multiple paths?

The fact that the element of the paths array can also be an array makes the API rather confusing.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can, however, this would require some modifications to the original code that uses _.omit() in the first place. Also, since this is exposed to the blocks.getSaveContent.extraProps filter, we'd have to provide full backward compatibility.

The existing code relies on supporting multiple paths and being able to specify them both as strings and arrays. So the API here strives to maintain backward compatibility while improving docs and providing unit tests to ensure the behavior is more transparent than before.

Note that this function is exported solely to allow it to be unit-tested. It should be for internal usage only and not part of the public API.

@@ -159,13 +278,13 @@ export function addSaveProps(
const skipSerialization = getBlockSupport( blockType, indicator );

if ( skipSerialization === true ) {
style = omit( style, path );
style = omitStyle( style, path );
Copy link
Member

Choose a reason for hiding this comment

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

Can path also be an array, or is it guaranteed to be a string?

Copy link
Member Author

Choose a reason for hiding this comment

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

path can be a string, an array, an array of strings, and an array of arrays.

Copy link
Member

Choose a reason for hiding this comment

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

If I pass { spacing: [ 'spacing', 'blockGap' ] } as skipPaths, then:

  • if skipSerialization is true, the omitStyle call will remove spacing and blockGap fields
  • if skipSerialization is [ 'feature' ], the omitStyle a few lines later will remove the spacing.blockGap.feature field

That seems wrong, we should always be removing spacing.blockGap, and never treat them separately.

Copy link
Member Author

Choose a reason for hiding this comment

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

I looked at the original code and couldn't fully understand why there was a need for a special treatment. However, from what I understand, this isn't something that's coming from this PR, so I believe it makes sense that we handle it in a different PR. Or am I misunderstanding you? What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have full context on the original implementation around skipPaths but when you check skipSerializationPathsEdit and skipSerializationPathsSave, their path values are all single element arrays.

I believe they were designed that way specifically to align with the fact that each block support (border, color, spacing, typography etc) corresponds with a single top-level path within the style object.

That leads me to conclude that there wasn't the intent to omit multiple style paths in a single call here, as proposed.

The initial call to omit a style occurs when an entire block support has its serialization skipped via a simple true value for its __experiementalSkipSerialization flag. For example, all typography styles. The next call is only to omit a single feature for a block support e.g. only skip text decoration but still serialize all other typography supports.

In the latter case, we want style.typography.textDecoration to be removed but the style.typography to remain.

I could well be misunderstanding your point @jsnajdr but hopefully the above helps clarify things some.

Copy link
Member

Choose a reason for hiding this comment

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

That leads me to conclude that there wasn't the intent to omit multiple style paths in a single call

Then why would skipPaths values be arrays? If the intent is to always omit only a single path, the skipPaths would be a Record< string, string >, like:

skipPaths = {
  'spacing': 'spacing.blockGap'
}

but in fact it is a Record< string, string[] >, like:

skipPaths = {
  'spacing': [ 'spacing.blockGap' ]
}

which IMO clearly shows that there can be potentially multiple paths to skip. The skipPaths structure is designed to support that. And indeed a call like:

paths = skipPaths[ 'spacing' ];
style = omit( style, paths );

would remove them all.

But the second omit call, the omit( style, [ [ ...path, feature ] ] ), uses path with different semantics. For our spacing example, that could lead to calling omit as:

omit( style, [ [ 'spacing.blockGap', 'blockGap' ] ] )

which is a nonsense combination, it won't omit anything.

The Gallery block has spacing support defined as:

supports: {
  spacing: {
    /* ... */
    __experimentalSkipSerialization: [ "blockGap" ],
  }
}

So, when calling addSaveProps on such a Gallery block, the spacing.blockGap style won't be removed although it should be?

However, from what I understand, this isn't something that's coming from this PR, so I believe it makes sense that we handle it in a different PR.

@tyxla It seems that the pre-existing code has a bug, caused by incorrect usage of omit. That seems very relevant to the current PR, and should get some attention. Otherwise the PR is kind of "garbage in, garbage out", not very valuable. Also, the PR doesn't modify anything else but addSaveProps, which is a reason why it's not interesting to land it separately without addressing issues.

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that the pre-existing code has a bug, caused by incorrect usage of omit

I think you might be right about a bug, but maybe not with the use of omit.

It appears to be an issue with the check for skipping serialization or the config for skipping the blockGap support in skipSerializationPathsSave.

The __experimentalSkipSerialization flags are meant to be a boolean or array. The block gap support looks like it was trying to always skip its serialization from being saved into block markup, likely due to that fact it was injected server-side.

The indicator used to retrieve the block support config, when processing that block gap entry from skipSerializationPathsSave, was simply spacing, which would return that entire supports config object.

I'd need to test that further, but it appears the block gap support wouldn't be being skipped for save at all, as that skip serialization value wouldn't be true or an array.

cc/ @andrewserong for your thoughts and additional background here.

Then why would skipPaths values be arrays?

Good point. Looking back through the file history here it looks like they were arrays due to some of the typography styles originally not being included under a single typography banner like they are now.

They could probably be changed to strings not arrays now.

But the second omit call, the omit( style, [ [ ...path, feature ] ] ), uses path with different semantics. For our spacing example, that could lead to calling omit as:

omit( style, [ [ 'spacing.blockGap', 'blockGap' ] ] )

A call as suggested here wouldn't occur as the skip serialization retrieved from the block supports wouldn't be true or an array.

So, when calling addSaveProps on such a Gallery block, the spacing.blockGap style won't be removed although it should be?

In this case, the blockGap style is being removed both in the edit and save contexts due to the normal spacing block support config in the skipPaths. When processing the skip paths, the indicator for the spacing support, ${ SPACING_SUPPORT_KEY }.__experimentalSkipSerialization, would return the array [ "blockGap" ]. Which would then mean a call to omit( style, [ [ 'spacing', 'blockGap' ] ] ).

Copy link
Contributor

Choose a reason for hiding this comment

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

Otherwise the PR is kind of "garbage in, garbage out", not very valuable. Also, the PR doesn't modify anything else but addSaveProps, which is a reason why it's not interesting to land it separately without addressing issues.

Keeping the scope for this PR purely to the replacement of the Lodash omit function sounds like a win. I don't think it hurts to move the discussion on the logic contained in addSaveProps to its own dedicated PR rather than hijack this PR further.

Happy to leave that call to @tyxla though 🙂

Copy link
Member

Choose a reason for hiding this comment

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

Now when we reached some agreement that there is indeed something wrong with the underlying code, and it's on someone's radar, my objections are no longer so strong 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, everyone for the thoughts and research put into this discussion.

@aaronrobertshaw basically said it here:

Keeping the scope for this PR purely to the replacement of the Lodash omit function sounds like a win. I don't think it hurts to move the discussion on the logic contained in addSaveProps to its own dedicated PR rather than hijack this PR further.

This aligns with my sentiment, and I do think we should try our best to keep changes focused on one thing at a time. What if there were 1000 bugs in this component? Should we fix them all in a single PR? I don't think so. That doesn't mean that this shouldn't be addressed in a follow-up, though.

I do feel like @aaronrobertshaw, @andrewserong, and @ramonjd are better equipped to address this one, but I'm happy to help with reviews and testing.

Any objections?

}

if ( Array.isArray( skipSerialization ) ) {
skipSerialization.forEach( ( featureName ) => {
const feature = renamedFeatures[ featureName ] || featureName;
style = omit( style, [ [ ...path, feature ] ] );
style = omitStyle( style, [ [ ...path, feature ] ] );
Copy link
Member

Choose a reason for hiding this comment

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

This indicates that path must be an array of path items, never a string, but the omitStyle( style, path ) above indicates otherwise: that it's either a string or an array of paths (not an array of one path's items). Confusing.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. Still the original omit() supported a single string. Would you recommend breaking backward compatibility in that case?

Copy link
Member

Choose a reason for hiding this comment

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

omitStyle is a new function -- which backward compatibility do we need to keep? The skipPaths argument of addSaveProps?

Copy link
Member Author

Choose a reason for hiding this comment

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

The skipPaths argument of addSaveProps?

Yes. Essentially, the function omitStyle() is used via the addSaveProps function and the blocks.getSaveContent.extraProps filter, and the skipPaths could technically be anything that we used to previously support with _.omit(). Let me know if that makes sense.

@jsnajdr
Copy link
Member

jsnajdr commented Oct 17, 2022

I have to admit that I don't understand the reasoning for this PR. Why change a util that is already loaded and always will be (lodash is loaded as a separate script in WP) with a custom utility that we'd have to maintain.

I think the reasoning for removing Lodash from Gutenberg, although the lodash WP script will practically always be loaded anyway, is that there are many usages of Gutenberg outside WordPress, and their number will grow.

@tyxla
Copy link
Member Author

tyxla commented Oct 17, 2022

Thanks for chiming in @youknowriad - with regards to why we're removing Lodash from Gutenberg, there are a bunch of rationales discussed in the links I've mentioned in the PR description. They're a great read and cover the state of things really well in my opinion.

On top of that, I agree and recognize that in the short term, the result is a few bytes on top of what we already had, but we're doing this with a few end goals in mind:

Removing lodash as a dependency from @wordpress packages

While this won't impact WP itself yet, since lodash will still be loaded in the admin, Gutenberg is more than just the editor for the WordPress core, and its packages are used by many projects outside of that context. Not having to add the entire Lodash library and worrying about its deduplication can be a big win for those projects, especially when trying to optimize for bundle size.

Not using lodash in the WP admin at all

AFAIK, the current usage of lodash inside the WP core is not intensive. Once it's no longer a dependency for the block editor, it should be pretty straightforward to stop using it completely, and just keep it as an available script for enqueueing. After all, why load a large library like this when most of it is not used?

Developer experience, code readability and quality

One of my primary focuses in working on Gutenberg on top of performance is improving the developer experience, and part of that is exploring ways to make code clearer and easier to read. This PR is a clear example of how using Lodash in certain places can make code difficult to read and understand, and conceal underlying functionality. The original _.omit() usage is extremely difficult to parse mentally, mostly because it requires one to understand how _.omit() really behaves under the hood, which isn't easy to be found in docs and is even more difficult to be found in its source code. Yes, we add another small function we need to maintain, but then it's well-covered by tests and well-documented, and that on its own improves the developer experience, and in my opinion that results in actually reducing the maintenance cost.

@sgomes
Copy link
Contributor

sgomes commented Oct 17, 2022

I really want to reinforce @tyxla's point that @wordpress packages are not just for the WP editor. They're being used in a number of projects as standalone libraries, or as part of a larger editor integration. It would be best to assume that not all of these projects will be using lodash (and they're not, in fact), and it would be best to avoid pushing such a large dependency onto them.

Plus, even in the context of WP core, we can't assume that a library will already be loaded, and that this will always be the case in the future. Even the ubiquitous jQuery has seen significant removal efforts, and can now easily be avoided by visitors of blogs with modern themes, that don't use jQuery-dependant plugins.

@youknowriad
Copy link
Contributor

Can we assume that the collection of all these small utils we'll be duplicating is smaller than the lodash script is self by a significant order of magnitude. Also, can't we just import lodash/omit and bundle it instead of maintaining our own utility?

I'm still very skeptical. I'm one of the persons that would encourage packages usage outside of WP and in different context but it still remains marginal to be honest compared to its potential.

@tyxla
Copy link
Member Author

tyxla commented Oct 17, 2022

Can we assume that the collection of all these small utils we'll be duplicating is smaller than the lodash script is self by a significant order of magnitude.

That's my assumption, yes, and it mirrors how the Lodash migration has been going so far. I don't think we'll end up duplicating too many of those. We've gotten rid of a big chunk of Lodash already and I don't see much duplication around the codebase.

I'm also positive that things will get better as more proposals TC39 proposals get accepted, standardized, and implemented by browsers. A few years ago, it was unthinkable to write good code without a library like Lodash, and nowadays, Lodash helper functions are almost unnecessary. Just imagine what it would mean for the possibilities of JavaScript a few years from now.

Note that what we're doing here is not really duplication; the substitute code is much simpler, shorter, more targeted and adapted to our specific needs, more readable, better documented, and better tested - I think we need to consider those important factors too.

Also, can't we just import lodash/omit and bundle it instead of maintaining our own utility?

Sure, we can. However, it's worth mentioning that it still pulls ~10% of the entire Lodash as a dependency (see here). That doesn't only make the bundle size way larger; it also makes the code less predictable, because as I stated above, it's not immediately clear how _.omit() works and how it handles different types of attributes.

I'm still very skeptical. I'm one of the persons that would encourage packages usage outside of WP and in different context but it still remains marginal to be honest compared to its potential.

@wordpress packages are, and will be used more broadly over time, and I believe we have to do our best to ease developers into using them for their projects. I'd like to think we do our best to improve the developer experience and we grasp every opportunity to make it better.

Anyway, I'd love to hear more about your opinion. I'm curious how all the trade-offs we've listed above are not enough to convince you 😉

expect( omitStyle( style, path ) ).toEqual( expected );
} );

it( 'should ignore a nullish style object', () => {
Copy link
Member

Choose a reason for hiding this comment

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

I like this PR!

Would it worth adding test to see what happens when there is no property match for a given path?

Copy link
Contributor

Choose a reason for hiding this comment

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

The extra test sounds like a good idea, even if just to protect against regressions for future refactors.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea indeed. I've added a test in a691f88.

Copy link
Contributor

@aaronrobertshaw aaronrobertshaw left a comment

Choose a reason for hiding this comment

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

Thanks for tackling the removal of Lodash dependencies @tyxla 👍

Personally, I'm in favour of the removal of omit here, given the arguments presented so far for doing so. Happy to let others make a final call on that, as I don't feel too strongly about it.

This PR tests well for me:

✅ Unit tests are passing
✅ Skipping of entire block supports or their individual features works
✅ Skipped serialization was working for static blocks e.g. Group, and dynamic blocks e.g. Search.

From what I could work out the blockGap related functionality was still working but it might be worth double checking with @andrewserong on that front.

packages/block-editor/src/hooks/test/style.js Outdated Show resolved Hide resolved
packages/block-editor/src/hooks/style.js Outdated Show resolved Hide resolved
expect( omitStyle( style, path ) ).toEqual( expected );
} );

it( 'should ignore a nullish style object', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The extra test sounds like a good idea, even if just to protect against regressions for future refactors.

packages/block-editor/src/hooks/style.js Outdated Show resolved Hide resolved
packages/block-editor/src/hooks/style.js Outdated Show resolved Hide resolved
tyxla and others added 3 commits October 18, 2022 13:31
Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
@tyxla
Copy link
Member Author

tyxla commented Oct 18, 2022

Thanks, everyone for the great feedback, insights, and thorough testing!

I think I've addressed all of it, and it's now ready for another look.

I agree with @jsnajdr that the skipPaths bug deserves attention in a separate PR, and from what I understand, we can get @aaronrobertshaw @andrewserong, and @ramonjd to help with it as well. I'm also happy to help with whatever I can.

const [ firstSubpath, ...restPath ] = path;
omitStyle( newStyle[ firstSubpath ], [ restPath ], true );
} else {
delete newStyle[ path ];
Copy link
Member

Choose a reason for hiding this comment

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

This is funny because it should be delete newStyle[ path[ 0 ] ], but it works anyway because [ 'x' ].toString() is still 'x'.

There should be also some check for path.length === 0 because otherwise

omit( { a: 1, '': 2 }, [ [] ] )

will be { a: 1 } while the correct result is the original object unchanged, and _.omit doesn't change the object either.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good eye, @jsnajdr! Both have been addressed in 4e3c1f9 and I've also added a unit test to cover the scenario you mentioned.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks! I looked at the changes and continue to approve this 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for double checking 🙌

@tyxla tyxla merged commit f1d0cdc into trunk Oct 19, 2022
@tyxla tyxla deleted the refactor/add-save-props-omit branch October 19, 2022 09:24
@github-actions github-actions bot added this to the Gutenberg 14.4 milestone Oct 19, 2022
Comment on lines +245 to +251
if ( path.length > 1 ) {
const [ firstSubpath, ...restPath ] = path;
omitStyle( newStyle[ firstSubpath ], [ restPath ], true );
} else if ( path.length === 1 ) {
delete newStyle[ path[ 0 ] ];
}
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

@tyxla: I have no idea if this code path is hot enough for this to make a difference in performance, but — just in case — there is this alternative to recursion (passes all tests):

	if (! obj) return obj

	if (! Array.isArray(paths)) paths = [paths]

	const copy = JSON.parse(JSON.stringify(obj));

	for (let path of paths) {
		if (! Array.isArray(path)) path = path.split('.')
		let target = copy;
		// Walk up to the last branch, stopping before the leaf
		for (let i = 0; i < path.length - 1; i++) {
			target = target[path[i]];
		}
		delete target[path[path.length - 1]];
	}

	return copy;

As a side benefit, it simplifies the interface by removing preserveReference.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, @mcsf! That seems to be the constant argument between implementing an algorithm to be recursive or iterative. I did have something like that but found the recursive version a bit more readable. Performance-wise, my observations are that both solutions will be close enough, with a difference of around roughly 10%. I'm happy to alter it to iterative if the recursive version doesn't look good to you though!

Copy link
Contributor

Choose a reason for hiding this comment

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

No worries, I trust your judgment! It was something I quickly drew up in between tasks, and I thought I'd preserve it in this discussion before I deleted the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Block editor /packages/block-editor [Type] Code Quality Issues or PRs that relate to code quality [Type] Performance Related to performance efforts
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

7 participants