leptos-rs / cargo-leptos

Build tool for Leptos (Rust)
MIT License
315 stars 88 forks source link

Implement precompression of static assets #165

Closed sebadob closed 8 months ago

sebadob commented 10 months ago

This is about adding a build step for cargo-leptos which does an optional precompression of the static assets in release build mode.

This provides a huge performance boost both on the server, which does not need to dynamically compress static files for each request, and in transit to the client, if the server does not do compression in some middleware. Additionally, it allows an easy embedding of precompressed static assets into the final binary itself, which improves the performance even further.

I have a first (still incomplete) implementation and will update the description after some testing.

sebadob commented 10 months ago

I did some testing today and used this version instead of the "original" cargo-leptos for the development and it works out very nicely so far.

The idea is, that you can specify the -P or --precompress flag, when you do a build -r or serve -r. This will precompress all static assets with gzip (for compatibility) and brotli. This greatly reduces the assets sizes of course and since these are static files, there is no need for a framework like axum or actix to do this compression each time over and over again with each request, when the results are static anyway.

What I am doing on my project is that am using rust_embed for embed the precompressed assets into the final binary for even better performance. You can either serve the precompressed files with a manual handler, for instance something like this:

use axum::body::Body;
use axum::extract::State;
use axum::{
    body::{boxed, Full},
    http::{header, Response, StatusCode},
    response,
};
use common::constants::DEV_MODE;
use http::{Request, Uri};
use leptos::*;
use std::borrow::Cow;
use tracing::{error, info};

#[derive(rust_embed::RustEmbed)]
#[folder = "../target/site/"]
struct Assets;

pub async fn file_and_error_handler(
    uri: Uri,
    State(_options): State<LeptosOptions>,
    req: Request<Body>,
) -> response::Response {
    let (_, path) = uri.path().split_at(1); // split off the first `/`
    let mime = mime_guess::from_path(path);

    let accept_encoding = req
        .headers()
        .get("accept-encoding")
        .map(|h| h.to_str().unwrap_or("none"))
        .unwrap_or("none");
    let (path, encoding) = if *DEV_MODE {
        // during DEV, don't care about the precompression -> faster workflow
        (Cow::from(path), "none")
    } else if accept_encoding.contains("br") {
        (Cow::from(format!("{}.br", path)), "br")
    } else if accept_encoding.contains("gzip") {
        (Cow::from(format!("{}.gz", path)), "gzip")
    } else {
        (Cow::from(path), "none")
    };

    match Assets::get(path.as_ref()) {
        Some(content) => {
            let body = boxed(Full::from(content.data));

            match *DEV_MODE {
                true => Response::builder()
                    .header(header::CONTENT_TYPE, mime.first_or_octet_stream().as_ref())
                    .header(header::CONTENT_ENCODING, encoding)
                    .body(body)
                    .unwrap(),
                false => {
                    Response::builder()
                        .header(header::CACHE_CONTROL, "max-age=86400")
                        .header(header::CONTENT_TYPE, mime.first_or_octet_stream().as_ref())
                        .header(header::CONTENT_ENCODING, encoding)
                        .body(body)
                        .unwrap()
                }
            }
        }

        None => {
            error!(">> Asset {} not found", path);
            for a in Assets::iter() {
                info!("Available asset: {}", a);
            }
            Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(boxed(Full::from("not found")))
                .unwrap()
        }
    }
}

Or if you do not want to directly embed the assets, with axum you can simply add somehting like this in your router:

ServeDir::new("../target/site/")
    .precompressed_br()
    .precompressed_gzip(),

For actix, there is an open issue to make it work just like with axum.

You can build or serve with this feature easily with cargo leptos build -r -P, which will then result in something like this:

Bildschirmfoto vom 2023-07-24 14-40-51

This precompression will be applied for --release builds only. It does not provide any advantage in local development, it would just slow down recompliation.

thestarmaker commented 10 months ago

Afternoon! This seem slike a very cool feature, what's the status on this?

sebadob commented 10 months ago

It's working perfectly fine, ready for review / merge. I just don't have the rights to merge it into master.

The conflicts on the Cargo.lock came up because a lot of other stuff was merged beforehand which updated the dependencies, and there were multiple. Someone with rights needs to "click the button".

gbj commented 8 months ago

Sorry for the long delay! I've been head down on 0.5 in my Leptos time for a while now, but have been reviewing this and the other open PRs and will be releasing a new version soon. Thanks so much for your work.