Skip to content

perf: reduce Open Graph module bundle size #150

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

Merged
merged 2 commits into from
Jan 13, 2024

Conversation

davidlj95
Copy link
Owner

@davidlj95 davidlj95 commented Jan 12, 2024

The only thing preventing myself from using my own library was performance. This modularity that has been built takes some bytes. And those bytes make the main bundle bigger. And a bigger bundle means it will take longer to fetch from the network & parse. Hence increasing the First Contentful Paint (FCP) as the main JS bundle blocks loading of the page. Or Time to Interactive (TTI) if FCP is solved by using server side rendering (SSR)

Tracking bundle size

To reduce the size of the bundle, first added tools to the CI to keep track of bundle size ( see #123 ). Then, when we're able to monitor that info, we can start optimising.

Optimisations

Then, inspected where are the bytes going. To do so, inspected the main JS bundle of the Angular v17 app production build. It includes the main features of the library due to the E2E tests. This means building the main JS bundle with esbuild. After inspecting minified bundles, discovered that:

@Injectable made some magic Angular bytes appear there to allow injection

See #112 Totally understandable. First try was to join all metadata per group (Open Graph, Twitter card, ...) in same class, so that code only exists once per module. However, see next point to why that's not ideal. Also doing it that way, we don't allow authors to manually remove certain metadata setters that won't ever be applied when injecting those manually.

To solve it, using factory providers instead. Indeed, a function that makes a factory provider, to avoid repeating the provider declaration around.

Classes cannot be easily optimised

First, they add some constructs class X extends Y that take some bytes.

Secondly, unused methods aren't removed given bundlers like esbuild (the one in v17), don't provide method devirtualization advanced optimisations. Indeed, that's why Angular uses Google's Closure Compiler. I assume others like webpack (used in Angular til v16) don't do. I recall checking it but not sure now tbh.

They also keep the names of class properties. Even if they're marked as private in Typescript (as that info is lost when compiling from TS to JS, so at bundling time it's not there anymore). And those can get quite long. You can mangle properties using rules when building but Angular doesn't provide easy access to the builder inner options (like esbuild opts) by design. So even if managing to do so, would be against Angular design and don't want to maintain that.

That's why functions are used instead of classes. Sorry OOP, I like you, but I prefer performance 😬. Also, SOLID practices can still be enforced using functions instead of classes anyway.

Functions have no class X extends Y syntax. Indeed, we can use arrow syntax which is super concise (instead of function). They also have no props. And argument names can be shortened when minifying. So if providing something as argument instead of being a class property, it can be renamed to something short. For instance superDuperLongService to a random short name like aX.

Object property names are not shortened

Minification process shortens names of identifiers (vars, functions, etc...) that are not part of global scope. However doesn't shorten object property names. Given they may be accessed with braces syntax like obj['foo']. So not safe to shorten them & ensure code runs properly given those dynamic use cases allowed. Classes will be instantiated into objects. So those method names / attributes won't be shortened. That's indeed why private typescript methods aren't shortened either. Cause that info is lost when compiling into JS. And it's JS the one being minified, so at that point minifier isn't sure if that's public or private and avoids shortening the name. Could try with JS private attributes tho 🤔

Anyway, property names are being kept as short as possible. Or removed to use consts outside of global scope that can be shortened.

Two equal literal strings aren't declared just once

They're declared many times. TBH not sure why. Maybe cause the minification doesn't go so far as they state in advanced optimizations section. So when we know two strings will be the same, we can create a const and refer to it to avoid that string literal appearing multiple times.

Experiment

Trying with Open Graph given it's mostly just setting og: meta properties. So shouldn't take the size it takes right now (2.7kBs). Code generated shouldn't be much more than the name of the JSON prop to look at + OG property to set. Which may be the same.

Result

See for yourself: #150 (comment) 🤭 🎉

65% (62% in webpack builds) of Open Graph module bytes are gone 👋 At cost of adding 124 bytes (3%) to core module. So 59% effective gain. And that's not even considering compression like gzip or brotli

Copy link
Owner Author

Current dependencies on/for this PR:

This stack of pull requests is managed by Graphite.

Copy link

github-actions bot commented Jan 12, 2024

📦 Bundle size (Angular v15)

Git ref: 92ad8f0

Module file Size Base size Difference
ngx-meta-core.mjs 3970 bytes (3.9KiB) 3848 bytes (3.8KiB) 3.17%: 122 bytes (122B)
ngx-meta-json-ld.mjs 779 bytes (779B) 779 bytes (779B) No change
ngx-meta-open-graph-profile.mjs 1463 bytes (1.5KiB) 1463 bytes (1.5KiB) No change
ngx-meta-open-graph.mjs 1019 bytes (1019B) 2734 bytes (2.7KiB) -62.72%: -1715 bytes (-1.7KiB)
ngx-meta-routing.mjs 1522 bytes (1.5KiB) 1522 bytes (1.5KiB) No change
ngx-meta-standard.mjs 3491 bytes (3.5KiB) 3491 bytes (3.5KiB) No change
ngx-meta-twitter-card.mjs 2169 bytes (2.2KiB) 2169 bytes (2.2KiB) No change

Copy link

github-actions bot commented Jan 12, 2024

📦 Bundle size (Angular v16)

Git ref: 92ad8f0

Module file Size Base size Difference
ngx-meta-core.mjs 3970 bytes (3.9KiB) 3854 bytes (3.8KiB) 3.00%: 116 bytes (116B)
ngx-meta-json-ld.mjs 779 bytes (779B) 779 bytes (779B) No change
ngx-meta-open-graph-profile.mjs 1463 bytes (1.5KiB) 1463 bytes (1.5KiB) No change
ngx-meta-open-graph.mjs 1019 bytes (1019B) 2734 bytes (2.7KiB) -62.72%: -1715 bytes (-1.7KiB)
ngx-meta-routing.mjs 1522 bytes (1.5KiB) 1522 bytes (1.5KiB) No change
ngx-meta-standard.mjs 3491 bytes (3.5KiB) 3491 bytes (3.5KiB) No change
ngx-meta-twitter-card.mjs 2169 bytes (2.2KiB) 2169 bytes (2.2KiB) No change

Copy link

github-actions bot commented Jan 12, 2024

📦 Bundle size (Angular v17)

Git ref: 92ad8f0

Module file Size Base size Difference
ngx-meta-core.mjs 3701 bytes (3.7KiB) 3577 bytes (3.5KiB) 3.46%: 124 bytes (124B)
ngx-meta-json-ld.mjs 600 bytes (600B) 600 bytes (600B) No change
ngx-meta-open-graph-profile.mjs 1274 bytes (1.3KiB) 1274 bytes (1.3KiB) No change
ngx-meta-open-graph.mjs 856 bytes (856B) 2470 bytes (2.5KiB) -65.34%: -1614 bytes (-1.6KiB)
ngx-meta-routing.mjs 1307 bytes (1.3KiB) 1307 bytes (1.3KiB) No change
ngx-meta-standard.mjs 3185 bytes (3.2KiB) 3185 bytes (3.2KiB) No change
ngx-meta-twitter-card.mjs 1894 bytes (1.9KiB) 1894 bytes (1.9KiB) No change

@davidlj95 davidlj95 force-pushed the stacked/perf-reduce-Open-Graph-module-bundle-size branch 2 times, most recently from 6919cd6 to 3c0c5fd Compare January 12, 2024 17:40
@davidlj95 davidlj95 force-pushed the stacked/perf-reduce-Open-Graph-module-bundle-size branch from 5a153f7 to 92ad8f0 Compare January 13, 2024 12:21
@davidlj95 davidlj95 mentioned this pull request Jan 13, 2024
@davidlj95 davidlj95 linked an issue Jan 13, 2024 that may be closed by this pull request
@davidlj95 davidlj95 merged commit dff2686 into main Jan 13, 2024
@davidlj95 davidlj95 deleted the stacked/perf-reduce-Open-Graph-module-bundle-size branch January 13, 2024 12:32
Copy link

🎉 This PR is included in version 1.0.0-alpha.17 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reduce bundle size
1 participant