Skip to content

Commit

Permalink
perf: added inbuilt minification
Browse files Browse the repository at this point in the history
This is behind the `minify` flag, which is enabled by default. This will
minify all HTML and CSS (but not JS due to an upstream bug) at
build-time and request-time. This reduced the size of the development
initial HTML load for the `core/basic` example by 20%.
  • Loading branch information
arctic-hen7 committed Sep 27, 2022
1 parent 2309c5c commit 50e04e0
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 4 deletions.
6 changes: 5 additions & 1 deletion packages/perseus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fs_extra = "1"
http = "0.2"
urlencoding = "2.1"
chrono = "0.4"
minify-html-onepass = "0.10"

[target.'cfg(target_arch = "wasm32")'.dependencies]
rexie = { version = "0.2", optional = true }
Expand All @@ -48,7 +49,7 @@ wasm-bindgen-futures = "0.4"
[features]
# Live reloading will only take effect in development, and won't impact production
# BUG This adds 1.9kB to the production bundle (that's without size optimizations though)
default = [ "live-reload", "hsr", "client-helpers", "macros", "dflt-engine" ]
default = [ "live-reload", "hsr", "client-helpers", "macros", "dflt-engine", "minify" ]
translator-fluent = ["fluent-bundle", "unic-langid", "intl-memoizer"]
translator-lightweight = []
# This feature adds support for a number of macros that will make your life MUCH easier (read: use this unless you have very specific needs or are completely insane)
Expand All @@ -57,6 +58,9 @@ macros = [ "perseus-macro" ]
dflt-engine = []
# This features enables client-side helpers designed to be run in the browser.
client-helpers = [ "console_error_panic_hook" ]
# This feature enables the minification of HTML, CSS, and JS assets, improving your page load speeds. You should only disable this if you're having issues with invalid
# HTML.
minify = []
# This feature enables Sycamore hydration by default (Sycamore hydration feature is always activated though)
# This is not enabled by default due to some remaining bugs (also, default features in Perseus can't be disabled without altering `.perseus/`)
hydrate = []
Expand Down
4 changes: 4 additions & 0 deletions packages/perseus/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::stores::{ImmutableStore, MutableStore};
use crate::template::Template;
use crate::template::{PageProps, TemplateMap};
use crate::translator::Translator;
use crate::utils::minify;
use futures::future::try_join_all;
use std::collections::HashMap;
use sycamore::prelude::SsrNode;
Expand Down Expand Up @@ -147,6 +148,7 @@ async fn gen_state_for_path(
let prerendered = sycamore::render_to_string(|cx| {
template.render_for_template_server(page_props.clone(), cx, translator)
});
minify(&prerendered, true)?;
// Write that prerendered HTML to a static file
mutable_store
.write(&format!("static/{}.html", full_path_encoded), &prerendered)
Expand All @@ -155,6 +157,7 @@ async fn gen_state_for_path(
// If the page also uses request state, amalgamation will be applied as for the
// normal content
let head_str = template.render_head_str(page_props, translator);
minify(&head_str, true)?;
mutable_store
.write(
&format!("static/{}.head.html", full_path_encoded),
Expand Down Expand Up @@ -184,6 +187,7 @@ async fn gen_state_for_path(
let prerendered = sycamore::render_to_string(|cx| {
template.render_for_template_server(page_props.clone(), cx, translator)
});
minify(&prerendered, true)?;
// Write that prerendered HTML to a static file
immutable_store
.write(&format!("static/{}.html", full_path_encoded), &prerendered)
Expand Down
7 changes: 7 additions & 0 deletions packages/perseus/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ pub enum ServerError {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
// We should only get a failure to minify if the user has given invalid HTML, or if Sycamore
// stuffed up somewhere
#[error("failed to minify html (you can disable the `minify` flag to avoid this; this is very likely a Sycamore bug, unless you've provided invalid custom HTML)")]
MinifyError {
#[source]
source: std::io::Error,
},
#[error(transparent)]
GlobalStateError(#[from] GlobalStateError),
#[error(transparent)]
Expand Down
20 changes: 17 additions & 3 deletions packages/perseus/src/server/html_shell.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use fmterr::fmterr;

use crate::error_pages::ErrorPageData;
use crate::page_data::PageData;
use crate::utils::minify;
use std::collections::HashMap;
use std::{env, fmt};

Expand Down Expand Up @@ -116,9 +119,9 @@ impl HtmlShell {
let port =
env::var("PERSEUS_RELOAD_SERVER_PORT").unwrap_or_else(|_| "3100".to_string());
scripts_before_boundary
.push(format!("window.__PERSEUS_RELOAD_SERVER_HOST = '{}'", host));
.push(format!("window.__PERSEUS_RELOAD_SERVER_HOST = '{}';", host));
scripts_before_boundary
.push(format!("window.__PERSEUS_RELOAD_SERVER_PORT = '{}'", port));
.push(format!("window.__PERSEUS_RELOAD_SERVER_PORT = '{}';", port));
}

// Add in the `<base>` element at the very top so that it applies to everything
Expand Down Expand Up @@ -337,6 +340,17 @@ impl fmt::Display for HtmlShell {
.replace(&html_to_replace_double, &html_replacement)
.replace(&html_to_replace_single, &html_replacement);

f.write_str(&new_shell)
// And minify everything
// Because this is run on live requests, we have to be fault-tolerant (if we
// can't minify, we'll fall back to unminified)
let minified = match minify(&new_shell, true) {
Ok(minified) => minified,
Err(err) => {
eprintln!("{}", fmterr(&err));
new_shell
}
};

f.write_str(&minified)
}
}
31 changes: 31 additions & 0 deletions packages/perseus/src/utils/minify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::errors::*;
use minify_html_onepass::{with_friendly_error, Cfg};

/// Minifies the given HTML document, including any CSS and JS. This is
/// *extremely* fast, and can be reasonably run before returning a request.
///
/// If the second argument is set to `false`, CSS and JS will not be minified,
/// and the performance will be improved.
pub(crate) fn minify(code: &str, minify_extras: bool) -> Result<String, ServerError> {
// In case the user is using invalid HTML (very tricky error to track down), we
// let them disable this feature
if cfg!(feature = "minify") {
let cfg = Cfg {
minify_js: false, // Pending wilsonzlin/minify-js#1
minify_css: minify_extras,
};
let mut bytes = code.as_bytes().to_vec();

match with_friendly_error(&mut bytes, &cfg) {
Ok(min_len) => Ok(std::str::from_utf8(&bytes[..min_len]).unwrap().to_string()),
Err(err) => Err(ServerError::MinifyError {
// We have to wrap this because the error types are non-`StdError`
// We want the error to be nice for the user (and it's not a security risk, since
// the HTML is being sent to the client anyway, if this is being run on the server)
source: std::io::Error::new(std::io::ErrorKind::NotFound, format!("{:#?}", err)),
}),
}
} else {
Ok(code.to_string())
}
}
4 changes: 4 additions & 0 deletions packages/perseus/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod decode_time_str;
#[cfg(target_arch = "wasm32")]
mod fetch;
mod log;
#[cfg(not(target_arch = "wasm32"))]
mod minify;
mod path_prefix;
#[cfg(target_arch = "wasm32")]
mod replace_head;
Expand All @@ -22,6 +24,8 @@ pub(crate) use context::provide_context_signal_replace;
pub use decode_time_str::{ComputedDuration, InvalidDuration, PerseusDuration}; /* These have dummy equivalents for the browser */
#[cfg(target_arch = "wasm32")]
pub(crate) use fetch::fetch;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use minify::minify;
pub use path_prefix::*;
#[cfg(target_arch = "wasm32")]
pub(crate) use replace_head::replace_head;

0 comments on commit 50e04e0

Please sign in to comment.