1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
/*!
This is a very simple plugin for [Perseus](https://arctic-hen7.github.io/perseus) that applies size optimizations automatically, which
decrease the size of your final Wasm bundle significantly, meaning faster loads for users because a smaller amount of data needs to be
transferred to their browsers. Because Perseus renders a page almost immediately through static generation, and then the Wasm bundle is
needed to make that page interactive, applying this plugin will decrease your app's time-to-interactive and its total blocking time
(a Lighthouse metric that normal Perseus apps don't do so well in on mobile).
If you're new to Perseus, check it out [on it's website](https://arctic-hen7.github.io/perseus) and [on GitHub](https://github.com/arctic-hen7/perseus)! Basically though, it's a really fast and fully-featured web framework for Rust!
## Usage
In your `src/lib.rs`, add the following to the bottom of the `define_app!` macro:
```rust
plugins: Plugins::new().plugin(perseus_size_opt(), SizeOpts::default())
```
If you have any other plugins defined, add the `.plugin()` call where appropriate. You'll also need to add the following imports:
```rust
use perseus_size_opt::{perseus_size_opt, SizeOpts};
```
Once that's done, run `perseus tinker` to apply the optimizations (this needs a separate command because they involve modifying the `.perseus/` directory), and then you can work with your app as normal!
If you ever want to uninstall the plugin, just remove the relevant `.plugin()` call and re-run `perseus tinker`, and it'll be completely removed.
## Optimizations
This plugin currently performs the following optimizations:
- `wee_alloc` -- an alternative allocator designed for Wasm that reduces binary size at the expense of slightly slower allocations
- `lto` -- reduces binary size when set to `true` by enabling link-time optimizations
- `opt-level` -- optimizes aggressively for binary size when set to `z`
- `codegen-units` -- makes faster and smaller code when set to lower values (but makes compile times slower in release mode)
Note that all optimizations will only apply to release builds. except for the use of `wee_alloc`, which will also affect development builds.
## Options
There are a few defaults available for setting size optimization levels, or you can build your own custom settings easily by constructing
`SizeOpts` manually.
- `::default()` -- enables all optimizations
- `::default_no_lto()` -- enables all optimizations except `lto = true`, because that can break compilation of execution on some hosting providers, like Netlify.
- `::only_wee_alloc()` -- only uses `wee_alloc`, applying no other optimizations
- `::no_wee_alloc()` -- applies all optimizations other than `wee_alloc`
## Stability
This plugin is considered quite stable due to how basic its optimizations are (the whole thing is one file), and so its stability is mostly dependent on that of Perseus. If you're happy to use Perseus, you shouldn't need to worry about using this plugin as well (in fact, it's recommended that all Perseus apps use this plugin).
*/
use cargo_toml::{Dependency, Manifest, Profile, Value};
use perseus::plugins::{empty_control_actions_registrar, Plugin, PluginAction, PluginEnv};
use perseus::Html;
use std::collections::BTreeMap;
use std::fs;
use thiserror::Error;
use toml::value::Map;
const PLUGIN_NAME: &str = "perseus-size-opt";
const WEE_ALLOC_DEF: &str = "#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;";
// This will need updating over time
const WEE_ALLOC_VERSION: &str = "0.4";
/// Options for size optimizations. Note that these settings will only affect release builds (e.g. when you run `perseus deploy`), not
/// development builds (except using `wee_alloc`, whcih will affect everything).
pub struct SizeOpts {
/// Whether or not to use `wee_alloc`, which reduces binary size significantly. This defaults to `true`. Note that this also makes
/// allocations slightly slower, so you'll need to decide what tradeoff between size and speed you want your app to have.
pub wee_alloc: bool,
/// Whether or not to use link-time optimizations, which defaults to `true`. If your app isn't recognized by providers like Netlify,
/// disable this.
pub lto: bool,
/// The optimization level to use, which defaults to `z` (aggressively optimize for speed). Cargo typically sets this to `3` for
/// release builds.
pub opt_level: String,
/// The value for the `codegen-units` property, which is set by default to 1 by this plugin. Higher values here will mean faster
/// compile times but slower code. The Rust default is 16 (256 with incremental builds).
pub codegen_units: u16,
/// Whether or not to enable the patch for `fluent-bundle` that fixes Perseus #83 (compiling taking forever with size optimizations). If you're running
/// Rust 2021, you should enable this until [this upstream issue](https://github.com/rust-lang/rust/issues/91011) is fixed.
pub enable_fluent_bundle_patch: bool,
}
impl Default for SizeOpts {
fn default() -> Self {
Self {
wee_alloc: true,
lto: true,
opt_level: "z".to_string(),
codegen_units: 1,
enable_fluent_bundle_patch: true,
}
}
}
// We add a few more sensible named defaults
impl SizeOpts {
/// The usual default, but without the `fluent-bundle` patch. Use this for greater size reductions if you're not using Rust 2021.
pub fn default_2018() -> Self {
Self {
wee_alloc: true,
lto: true,
opt_level: "z".to_string(),
codegen_units: 1,
enable_fluent_bundle_patch: false,
}
}
/// The usual default, but without `lto` enabled, which is known to cause problems on some hosting services like Netlify. If your
/// app runs out of memory during compilation, or won't be served properly, try this.
pub fn default_no_lto() -> Self {
Self {
wee_alloc: true,
lto: false,
opt_level: "z".to_string(),
codegen_units: 1,
enable_fluent_bundle_patch: true,
}
}
/// Only enables the alternative allocator `wee_alloc`, with no further additional optimizations made.
pub fn only_wee_alloc() -> Self {
Self {
wee_alloc: true,
lto: false,
opt_level: "3".to_string(),
codegen_units: 16,
enable_fluent_bundle_patch: true,
}
}
/// Enables all optimizations other than changing the default allocator.
pub fn no_wee_alloc() -> Self {
Self {
wee_alloc: false,
lto: true,
opt_level: "z".to_string(),
codegen_units: 1,
enable_fluent_bundle_patch: true,
}
}
}
/// The errors that this plugin can return.
#[derive(Error, Debug)]
pub enum Error {
#[error("couldn't get and parse `.perseus/Cargo.toml`, try running `perseus tinker` again (without the `--no-clean` option)")]
GetManifestFailed {
#[source]
source: cargo_toml::Error,
},
#[error("couldn't update `.perseus/Cargo.toml`, try running `perseus tinker` again (without the `--no-clean` option)")]
WriteManifestFailed {
#[source]
source: std::io::Error,
},
#[error("couldn't read `.perseus/src/lib.rs`, try running `perseus tinker` again (without the `--no-clean` option)")]
ReadLibFailed {
#[source]
source: std::io::Error,
},
#[error("couldn't update `.perseus/src/lib.rs`, try running `perseus tinker` again (without the `--no-clean` option)")]
WriteLibFailed {
#[source]
source: std::io::Error,
},
}
/// The actual mechanics of this plugin, which apply size optimizations to `.perseus/Cargo.toml` and `.perseus/src/lib.rs`.
fn apply_size_opts(opts: &SizeOpts) -> Result<(), Error> {
// Get the internal `Cargo.toml` file in `.perseus/` (the current directory)
let mut manifest = Manifest::from_path("Cargo.toml")
.map_err(|err| Error::GetManifestFailed { source: err })?;
// Apply size optimizations to the release profile
let mut release_profile = manifest.profile.release.unwrap_or(
// Because `Default` is not implemented for this `struct`...
Profile {
opt_level: None,
lto: None,
debug: None,
rpath: None,
debug_assertions: None,
codegen_units: None,
panic: None,
incremental: None,
overflow_checks: None,
package: std::collections::BTreeMap::default(),
build_override: None,
},
);
release_profile.opt_level = Some(opts.opt_level.clone().into());
release_profile.lto = Some(opts.lto.into());
release_profile.codegen_units = Some(opts.codegen_units); // If the `fluent-bundle` patch is enabled, apply it
// TODO Remove this patch entirely once the upstream issue in LLVM is fixed and the error no longer occurs
if opts.enable_fluent_bundle_patch {
let mut fluent_bundle_conf = Map::new();
fluent_bundle_conf.insert("opt-level".to_string(), Value::Integer(2));
let mut patch = BTreeMap::new();
patch.insert(
"fluent-bundle".to_string(),
Value::Table(fluent_bundle_conf),
);
release_profile.package = patch;
}
manifest.profile.release = Some(release_profile);
// Add `wee_alloc` as a dependency if we're using that optimization
if opts.wee_alloc {
// We override any existing versions of `wee_alloc`, the user can disable that optimization if they're doing more advanced stuff
manifest.dependencies.insert(
"wee_alloc".to_string(),
Dependency::Simple(WEE_ALLOC_VERSION.to_string()),
);
}
// For some reason, our modifications to the manifest wipe out the `cdylib` definition (TODO investigate this further), so we need to add it back
let mut manifest_lib = manifest.lib.unwrap_or_default();
manifest_lib.crate_type = Some(vec!["cdylib".to_string(), "rlib".to_string()]);
manifest.lib = Some(manifest_lib);
// Write the new manifest
// This will result in a ton of previously implied stuff being written, so the manifest will get a lot longer (fine because it's internal)
let manifest_str = toml::to_string(&manifest).unwrap();
fs::write("Cargo.toml", manifest_str)
.map_err(|err| Error::WriteManifestFailed { source: err })?;
if opts.wee_alloc {
// Again, this is inside `.perseus/`, we're modifying the engine, not the user's code
let lib_contents =
fs::read_to_string("src/lib.rs").map_err(|err| Error::ReadLibFailed { source: err })?;
//
// Prepend the new allocator definition to the file
// We actually need to put it on the second line after a module-level attribute
let lib_contents_with_wee_alloc = lib_contents.replace(
"#![allow(clippy::unused_unit)]",
&format!("#![allow(clippy::unused_unit)]\n{}\n", WEE_ALLOC_DEF),
);
fs::write("src/lib.rs", lib_contents_with_wee_alloc)
.map_err(|err| Error::WriteLibFailed { source: err })?;
}
Ok(())
}
/// Gets the plugin itself to be handed to Perseus' `define_app!` macro. Note that this plugin's optimizations will only take effect
/// in release mode (e.g. when you run `perseus deploy`).
pub fn perseus_size_opt<G: Html>() -> Plugin<G, SizeOpts> {
Plugin::new(
PLUGIN_NAME,
|mut actions| {
actions
.tinker
.register_plugin(PLUGIN_NAME, |_, plugin_data| {
if let Some(plugin_data) = plugin_data.downcast_ref::<SizeOpts>() {
let res = apply_size_opts(plugin_data);
if let Err(err) = res {
panic!("error in `perseus-size-opt`: {}", err);
}
} else {
unreachable!();
}
});
actions
},
empty_control_actions_registrar,
// This plugin only needs to run at tinker-time, otherwise it increases binary sizes
PluginEnv::Server,
)
}