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

Replace sprockets-rails with propshaft #1910

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

jagthedrummer
Copy link
Contributor

@jagthedrummer jagthedrummer commented Feb 6, 2025

propshaft is the new gem for the asset pipeline that replaces sprockets. Moving to propshaft from sprockets should make it easier to transition to using importmaps.

propshaft takes a more simplified approach and doesn't provide as many features as sprockets, but I don't think that we were really depending any of those features. We already use jsbuilding-rails and cssbundling-rails to handle bundling and transpiling .js and .css files.

After making these changes I ran rails assets:clobber to make sure that I wasn't seeing old assets, and then I ran bin/dev to launch the dev server. Everything seems to work as expected. I confirmed that the color picker, date picker, super selects, dependent fields, and mobile slide out menu are all working.

I ran rails assets:precompile to make sure that precompilation works.

Details about differences between output under propshaft and sprockets

I ran the following to compare precompilation output from sprockets with the output from propshaft both in production mode and in development mode. (The main branch is using sprockets, and the jeremy/propshaft branch is using propshaft.)

git checkout main
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 rails assets:precompile
mv public/assets public/assets_sprockets_production

RAILS_ENV=development SECRET_KEY_BASE_DUMMY=1 rails assets:precompile
mv public/assets public/assets_sprockets_development

git checkout jeremy/propshaft
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 rails assets:precompile
mv public/assets public/assets_propshaft_production

RAILS_ENV=development SECRET_KEY_BASE_DUMMY=1 rails assets:precompile
mv public/assets public/assets_propshaft_development

The biggest differences that I can see are:

  • propshaft picks up many more files for precompilation than sprockets
  • propshaft does not produce gzipped/compressed versions of the files
  • propshaft does not obfuscate .js code in production mode

propshaft picks up many more files for precompilation than sprockets

There's a considerable difference in the size of the generated directories.

du -sh assets_*
 37M    assets_propshaft_development
 37M    assets_propshaft_production
 14M    assets_sprockets_development
 12M    assets_sprockets_production

That size difference is because sprockets only precompiles assets that are explicitly requested via config.assets_paths but propshaft grabs everything in an app/assets directory, both from the application itself and from any gems.

sprockets produces 4 directories containing 86 files.
propshaft produces 21 directories containing 1787 files

If we need to prevent compiliation of some of these additional files with propshaft we can use the config.assets.excluded_paths configuration option.

See this gist for a comparison between tree assets_propshaft_production/ and tree assets_sprockets_production/. (Including that info here makes the PR description too long for GitHub validations. 😢)

propshaft does not produce gzipped/compressed versions of the files

Using sprockets the precompiliation process will produce gzipped versions of assets, but propshaft does not.

ls assets_sprockets/application*
assets_sprockets/application-56aa2d6ac0411d27a31d2d772b59c17737048554938a85578260ee30cc4e2810.css
assets_sprockets/application-56aa2d6ac0411d27a31d2d772b59c17737048554938a85578260ee30cc4e2810.css.gz
assets_sprockets/application-a181fae3aa80f6a405086698e4f326c831e56bc5e2fdf819e8c744824e7fe567.js
assets_sprockets/application-a181fae3aa80f6a405086698e4f326c831e56bc5e2fdf819e8c744824e7fe567.js.gz
assets_sprockets/application.css-14cf1b41f7b6abfad8bc1ac76212b160d04fe20361c6205ddf2a747fb5fc3cf0.map
assets_sprockets/application.css-14cf1b41f7b6abfad8bc1ac76212b160d04fe20361c6205ddf2a747fb5fc3cf0.map.gz
assets_sprockets/application.js-29150f03941c8fdc7a88d1ed0a863e42b64978b8cdf00f49c2704d7e259febf1.map
assets_sprockets/application.js-29150f03941c8fdc7a88d1ed0a863e42b64978b8cdf00f49c2704d7e259febf1.map.gz
assets_sprockets/application.light-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js
assets_sprockets/application.light-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js.gz
assets_sprockets/application.light-7f8e2dd7acbcc6e25b2647ecddd41a9e78f11fffac285dd61c34ae40ae4728cb.css
assets_sprockets/application.light-7f8e2dd7acbcc6e25b2647ecddd41a9e78f11fffac285dd61c34ae40ae4728cb.css.gz
assets_sprockets/application.light.js-8126603a99003c29abd335aedc35194359cf35d0798939e63481a47cc7b7c1ad.map
assets_sprockets/application.light.js-8126603a99003c29abd335aedc35194359cf35d0798939e63481a47cc7b7c1ad.map.gz
assets_sprockets/application.mailer.light-bba9838096d4f808809a91d57d903959ace1bcedddd6b495f9bca246cc3fd6be.css
assets_sprockets/application.mailer.light-bba9838096d4f808809a91d57d903959ace1bcedddd6b495f9bca246cc3fd6be.css.gz
ls assets_propshaft/application*
assets_propshaft/application-3b1dadb9.css.map
assets_propshaft/application-3bbac6f0.js
assets_propshaft/application-cf8df722.css
assets_propshaft/application-fd45fee3.js.map
assets_propshaft/application.light-6d57f58c.css
assets_propshaft/application.light-b0df6176.js.map
assets_propshaft/application.light-d9ebbb44.js
assets_propshaft/application.mailer.light-97f9db98.css

Many webservers/CDNs can dynamically compress assets as they're being served, so this may not be an issue. If we (or a downstream app) need to produce gzipped versions we could handle that in a separate post-precompilation step.

find public/assets -type f -exec gzip -k {} \;

Precompiled File Content Differences

The output of the generated .css files is almost identical, with the only differences being related to fingerprinting and sourcemap locations.

$ diff assets_sprockets_production/application-56aa2d6ac0411d27a31d2d772b59c17737048554938a85578260ee30cc4e2810.css assets_propshaft_production/application-cf8df722.css
1c1
<
---
> @charset "UTF-8";
1980c1980
<   background-image: url(/assets/flags-645KQKNA-959070a9f002abd28383322dd455a851d1fd445974edb3f720d54ff79894e28b.png);
---
>   background-image: url("/assets/flags-645KQKNA-939e7450.png");
1987c1987
<     background-image: url(/assets/flags@2x-E4CMA2OR-d00ec77cf49d0c3fbd725dbcdcca661b5db35a02d12f8f4fcf8a3ce6065391bc.png);
---
>     background-image: url("/assets/[email protected]");
3034c3034
<   src: url(/assets/themify-icons-AUCIARCF-f1ba2ff6b8910c974fe48b17a80843b8b19ac6e6ae08d68bd27df2259ce6c658.eot);
---
>   src: url("/assets/themify-icons-AUCIARCF-fcb0a881.eot");
3036,3039c3036,3039
<     url(/assets/themify-icons-AUCIARCF-f1ba2ff6b8910c974fe48b17a80843b8b19ac6e6ae08d68bd27df2259ce6c658.eot?#iefix) format("eot"),
<     url(/assets/themify-icons-LDWN3OQG-efcb3c913adebf3e17d241a55cab2c25f0ff6fbd217d1ae4c29e7c84952a404b.woff) format("woff"),
<     url(/assets/themify-icons-NS22GCUV-67c745cef69ad6303b7cf19bd616a48401e7bb8e1e1d9de050c7d6622c56fcb5.ttf) format("truetype"),
<     url(/assets/themify-icons-KIKGDMUW-65e509ce3dede84abcba9340e383d7188b5fd05d30a7558ad4b7bc2c8d8d1118.svg#themify-icons) format("svg");
---
>     url("/assets/themify-icons-AUCIARCF-fcb0a881.eot?#iefix") format("eot"),
>     url("/assets/themify-icons-LDWN3OQG-10a0179e.woff") format("woff"),
>     url("/assets/themify-icons-NS22GCUV-74860cfe.ttf") format("truetype"),
>     url("/assets/themify-icons-KIKGDMUW-8828ac07.svg#themify-icons") format("svg");
4110c4110
< /*# sourceMappingURL=application.css.map */
---
> /*# sourceMappingURL=/assets/application-3b1dadb9.css.map */

For .js files, in production mode they are uglified/obfuscated with sprockets but not with propshaft. But in development mode the output is almost identical. The only difference is related to the sourcemap location.

$ diff assets_sprockets_development/application-b19fa5cf2669081d1f12e5fa25740a3727183a87095a665dbd9639edb7424553.js assets_propshaft_development/application-3bbac6f0.js
46591,46593c46591
< //# sourceMappingURL=/assets/application.js-29150f03941c8fdc7a88d1ed0a863e42b64978b8cdf00f49c2704d7e259febf1.map
< //!
< ;
---
> //# sourceMappingURL=/assets/application-fd45fee3.js.map

There is a decent difference in size of the compiled js file in production mode. This is due to the obfuscation step also removing line breaks and using shorter obfuscated variable names.

ls -al assets_sprockets_production/application-a181fae3aa80f6a405086698e4f326c831e56bc5e2fdf819e8c744824e7fe567.js assets_propshaft_production/application-3bbac6f0.js
-rw-r--r--  1 jgreen  staff   3.4M Feb  6 10:43 assets_propshaft_production/application-3bbac6f0.js
-rw-r--r--  1 jgreen  staff   2.5M Feb  6 10:43 assets_sprockets_production/application-a181fae3aa80f6a405086698e4f326c831e56bc5e2fdf819e8c744824e7fe567.js

We could potentially use something like esbuild-plugin-obfuscator to obfuscate the code during the esbuild process. But ideally we'll be able to move away from using esbuild at all once we've migrated to importmaps, so it may not be worth it to complicate things with that plugin.

After manually gzipping the production assets that propshaft produces they're not that much larger than the gzipped version that sprockets produces.

ls -al assets_sprockets_production/application-a181fae3aa80f6a405086698e4f326c831e56bc5e2fdf819e8c744824e7fe567.js.gz assets_propshaft_production/application-3bbac6f0.js.gz
-rw-r--r--  1 jgreen  staff   814K Feb  6 10:43 assets_propshaft_production/application-3bbac6f0.js.gz
-rw-r--r--  1 jgreen  staff   701K Feb  6 10:43 assets_sprockets_production/application-a181fae3aa80f6a405086698e4f326c831e56bc5e2fdf819e8c744824e7fe567.js.gz

How to test this PR

To test this PR you can do the following to start a new BT project that uses this branch as the base (instead of the main branch from the starter repo).

git clone -b jeremy/propshaft [email protected]:bullet-train-co/bullet_train.git propshaft_test
cd propshaft_test
bin/configure
bin/setup

Copy link
Member

@pascallaliberte pascallaliberte left a comment

Choose a reason for hiding this comment

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

@jagthedrummer Just testing with another BT app that had a .css.erb file, which sprockets just picked up automatically, but not propshaft.

Maybe we'll need to document that issue as part of the upgrade doc when shipping this?

Otherwise, my only other concern is about asset sizes on Heroku and Render out of the box. Maybe BT's config files for Heroku on Render need to be tweaked? I don't know enough about that.

@jagthedrummer
Copy link
Contributor Author

Thanks for taking a look, @pascallaliberte! I think those are both good suggestions. We should definitely call out the .css.erb thing, probably both in the release notes, and maybe also in the customization docs. And as far as app size goes, I think that with some good rules added to config.assets.excluded_paths we can trim down the asset folder quiet a bit.

@jagthedrummer
Copy link
Contributor Author

I did some work on configuring excluded_paths and was able to get the precompiled public/assets directory slimmed down considerably.

$ du -sh public/assets/
 11M    public/assets/
$ tree public/assets/
public/assets/
├── action_cable-5212cfee.js
├── actioncable-ac25813f.js
├── actioncable.esm-e0ec9819.js
├── actiontext-a4ee937e.js
├── actiontext.esm-f1c04d34.js
├── activestorage-32201f68.js
├── activestorage.esm-f2909226.js
├── application-3b1dadb9.css.map
├── application-3bbac6f0.js
├── application-cf8df722.css
├── application-fd45fee3.js.map
├── application.light-6d57f58c.css
├── application.light-b0df6176.js.map
├── application.light-d9ebbb44.js
├── application.mailer.light-97f9db98.css
├── bullet_train_api_manifest-e8dc057d.js
├── bullet_train_fields_manifest-e8dc057d.js
├── bullet_train_has_uuid_manifest-e8dc057d.js
├── bullet_train_incoming_webhooks_manifest-e8dc057d.js
├── bullet_train_integrations_manifest-e8dc057d.js
├── bullet_train_integrations_stripe_manifest-e8dc057d.js
├── bullet_train_manifest-e8dc057d.js
├── bullet_train_obfuscates_id_manifest-e8dc057d.js
├── bullet_train_outgoing_webhooks_manifest-e8dc057d.js
├── bullet_train_scope_questions_manifest-e8dc057d.js
├── bullet_train_sortable_manifest-e8dc057d.js
├── bullet_train_super_load_and_authorize_resource_manifest-e8dc057d.js
├── bullet_train_super_scaffolding_manifest-e8dc057d.js
├── bullet_train_themes_manifest-e8dc057d.js
├── bullet_train_themes_tailwind_css_manifest-e8dc057d.js
├── cable_ready-b6a5283d.js
├── cable_ready.umd-1a197063.js
├── doorkeeper
│   ├── admin
│   │   └── application-f0af4414.css
│   └── application-b9cfe656.css
├── flags-645KQKNA-939e7450.png
├── [email protected]
├── intl-tel-input-utils-72ccce08.js.map
├── intl-tel-input-utils-92bb1840.js
├── logo
│   ├── favicon-fa4690a4.png
│   ├── icon-acc1c7a7.png
│   └── logo-35f4024c.png
├── prism-ed239b4f.css
├── prism.min-d9907492.js
├── products
│   ├── rocket-7e4c8bcf.png
│   └── sparkles-fbdc0047.png
├── rails-ujs-20eaf715.js
├── rails-ujs.esm-e925103b.js
├── showcase-b6ffae62.js
├── showcase-ec7ed604.css
├── showcase.highlights-c808ab1f.css
├── showcase.tailwind-ddc2ced8.css
├── showcase_manifest-be2c1fc6.js
├── stimulus-autoloader-9d447422.js
├── stimulus-d59b3b7f.js
├── stimulus-importmap-autoloader-64cc03e1.js
├── stimulus-loading-1fc53fe7.js
├── stimulus.min-2395e199.js.map
├── stimulus.min-4b1e420e.js
├── themify-icons-AUCIARCF-fcb0a881.eot
├── themify-icons-KIKGDMUW-8828ac07.svg
├── themify-icons-LDWN3OQG-10a0179e.woff
├── themify-icons-NS22GCUV-74860cfe.ttf
├── trix-0d050b00.js
├── trix-c4e7be2d.css
├── turbo-86e38c3c.js
├── turbo.min-5dd5a71a.js.map
└── turbo.min-fae85750.js

4 directories, 67 files

That's with this configuration in config/initializers/assets.rb

# These prevent `propshaft` from adding a bunch of extra, unused assets to publis/assets during precompilation.
Rails.application.config.assets.excluded_paths = [
  Avo::Engine.root.join("app/assets/builds"),
  Avo::Engine.root.join("app/assets/config"),
  Avo::Engine.root.join("app/assets/stylesheets"),
  Avo::Engine.root.join("app/assets/svgs"),
  Cloudinary::Engine.root.join("vendor/assets/html"),
  Cloudinary::Engine.root.join("vendor/assets/javascripts"),
  Doorkeeper::Engine.root.join("vendor/assets/stylesheets"),
  BulletTrain::Themes::Light::Engine.root.join("app/assets/config"),
  BulletTrain::Themes::Light::Engine.root.join("app/assets/stylesheets"),
]

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

Successfully merging this pull request may close these issues.

2 participants