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

feat: support performance config #6456

Merged
merged 6 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/node_binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export enum BuiltinPluginName {
CssModulesPlugin = 'CssModulesPlugin',
APIPlugin = 'APIPlugin',
RuntimeChunkPlugin = 'RuntimeChunkPlugin',
SizeLimitsPlugin = 'SizeLimitsPlugin',
HttpExternalsRspackPlugin = 'HttpExternalsRspackPlugin',
CopyRspackPlugin = 'CopyRspackPlugin',
HtmlRspackPlugin = 'HtmlRspackPlugin',
Expand Down Expand Up @@ -1266,6 +1267,13 @@ export interface RawRuntimeChunkOptions {
name: string | ((entrypoint: { name: string }) => string)
}

export interface RawSizeLimitsPluginOptions {
assetFilter?: (assetFilename: string) => boolean
hints?: "error" | "warning"
maxAssetSize?: number
maxEntrypointSize?: number
}

export interface RawSnapshotOptions {
resolve: RawSnapshotStrategy
module: RawSnapshotStrategy
Expand Down
1 change: 1 addition & 0 deletions crates/rspack_binding_options/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ rspack_plugin_remove_empty_chunks = { path = "../rspack_plugin_remove_empty_
rspack_plugin_runtime = { path = "../rspack_plugin_runtime" }
rspack_plugin_runtime_chunk = { path = "../rspack_plugin_runtime_chunk" }
rspack_plugin_schemes = { path = "../rspack_plugin_schemes" }
rspack_plugin_size_limits = { path = "../rspack_plugin_size_limits" }
rspack_plugin_split_chunks = { path = "../rspack_plugin_split_chunks" }
rspack_plugin_swc_css_minimizer = { path = "../rspack_plugin_swc_css_minimizer" }
rspack_plugin_swc_js_minimizer = { path = "../rspack_plugin_swc_js_minimizer" }
Expand Down
10 changes: 10 additions & 0 deletions crates/rspack_binding_options/src/options/raw_builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod raw_limit_chunk_count;
mod raw_mf;
mod raw_progress;
mod raw_runtime_chunk;
mod raw_size_limits;
mod raw_swc_js_minimizer;
mod raw_to_be_deprecated;

Expand Down Expand Up @@ -60,6 +61,7 @@ use rspack_plugin_runtime::{
};
use rspack_plugin_runtime_chunk::RuntimeChunkPlugin;
use rspack_plugin_schemes::{DataUriPlugin, FileUriPlugin};
use rspack_plugin_size_limits::SizeLimitsPlugin;
use rspack_plugin_swc_css_minimizer::SwcCssMinimizerRspackPlugin;
use rspack_plugin_swc_js_minimizer::SwcJsMinimizerRspackPlugin;
use rspack_plugin_warn_sensitive_module::WarnCaseSensitiveModulesPlugin;
Expand All @@ -79,6 +81,7 @@ use self::{
raw_css_extract::RawCssExtractPluginOption,
raw_mf::{RawConsumeSharedPluginOptions, RawContainerReferencePluginOptions, RawProvideOptions},
raw_runtime_chunk::RawRuntimeChunkOptions,
raw_size_limits::RawSizeLimitsPluginOptions,
};
use crate::{
plugins::{CssExtractRspackAdditionalDataPlugin, JsLoaderResolverPlugin},
Expand Down Expand Up @@ -147,6 +150,7 @@ pub enum BuiltinPluginName {
CssModulesPlugin,
APIPlugin,
RuntimeChunkPlugin,
SizeLimitsPlugin,

// rspack specific plugins
// naming format follow XxxRspackPlugin
Expand Down Expand Up @@ -402,6 +406,12 @@ impl BuiltinPlugin {
RuntimeChunkPlugin::new(downcast_into::<RawRuntimeChunkOptions>(self.options)?.into())
.boxed(),
),
BuiltinPluginName::SizeLimitsPlugin => {
let plugin =
SizeLimitsPlugin::new(downcast_into::<RawSizeLimitsPluginOptions>(self.options)?.into())
.boxed();
plugins.push(plugin)
}

// rspack specific plugins
BuiltinPluginName::HttpExternalsRspackPlugin => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use derivative::Derivative;
use napi_derive::napi;
use rspack_napi::threadsafe_function::ThreadsafeFunction;
use rspack_plugin_size_limits::{AssetFilterFn, SizeLimitsPluginOptions};

#[derive(Derivative)]
#[derivative(Debug)]
#[napi(object, object_to_js = false)]
pub struct RawSizeLimitsPluginOptions {
#[derivative(Debug = "ignore")]
#[napi(ts_type = "(assetFilename: string) => boolean")]
pub asset_filter: Option<ThreadsafeFunction<String, bool>>,
#[napi(ts_type = "\"error\" | \"warning\"")]
pub hints: Option<String>,
pub max_asset_size: Option<f64>,
pub max_entrypoint_size: Option<f64>,
}

impl From<RawSizeLimitsPluginOptions> for SizeLimitsPluginOptions {
fn from(value: RawSizeLimitsPluginOptions) -> Self {
SizeLimitsPluginOptions {
asset_filter: value.asset_filter.map(|asset_filter| {
let asset_filter_fn: AssetFilterFn = Box::new(move |name| {
let f = asset_filter.clone();

Box::pin(async move { f.call(name.to_owned()).await })
});
asset_filter_fn
}),
hints: value.hints,
max_asset_size: value.max_asset_size,
max_entrypoint_size: value.max_entrypoint_size,
}
}
}
16 changes: 16 additions & 0 deletions crates/rspack_plugin_size_limits/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
edition = "2021"
license = "MIT"
name = "rspack_plugin_size_limits"
repository = "https://github.com/web-infra-dev/rspack"
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
derivative = { workspace = true }
futures = { workspace = true }
rspack_core = { path = "../rspack_core" }
rspack_error = { path = "../rspack_error" }
rspack_hook = { path = "../rspack_hook" }
rspack_util = { path = "../rspack_util" }
240 changes: 240 additions & 0 deletions crates/rspack_plugin_size_limits/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use std::fmt::Debug;

use derivative::Derivative;
use futures::future::BoxFuture;
use rspack_core::{
ApplyContext, ChunkGroup, Compilation, CompilationAsset, CompilerAfterEmit, CompilerOptions,
Plugin, PluginContext,
};
use rspack_error::{Diagnostic, Result};
use rspack_hook::{plugin, plugin_hook};
use rspack_util::size::format_size;

pub type AssetFilterFn = Box<dyn for<'a> Fn(&'a str) -> BoxFuture<'a, Result<bool>> + Sync + Send>;

#[derive(Derivative)]
#[derivative(Debug)]
pub struct SizeLimitsPluginOptions {
#[derivative(Debug = "ignore")]
pub asset_filter: Option<AssetFilterFn>,
pub hints: Option<String>,
pub max_asset_size: Option<f64>,
pub max_entrypoint_size: Option<f64>,
}

#[plugin]
#[derive(Debug)]
pub struct SizeLimitsPlugin {
options: SizeLimitsPluginOptions,
}

impl SizeLimitsPlugin {
pub fn new(options: SizeLimitsPluginOptions) -> Self {
Self::new_inner(options)
}

async fn asset_filter(&self, name: &str, asset: &CompilationAsset) -> bool {
let asset_filter = &self.options.asset_filter;

if let Some(asset_filter) = asset_filter {
asset_filter(name)
.await
.expect("run SizeLimitsPlugin asset filter error")
} else {
!asset.info.development
}
}

async fn get_entrypoint_size(&self, entrypoint: &ChunkGroup, compilation: &Compilation) -> f64 {
let mut size = 0.0;

for filename in entrypoint.get_files(&compilation.chunk_by_ukey) {
let asset = compilation.assets().get(&filename);

if let Some(asset) = asset {
if !self.asset_filter(&filename, asset).await {
continue;
}

let source = asset.get_source();

if let Some(source) = source {
size += source.size() as f64;
}
}
}

size
}

fn add_diagnostic(
hints: &str,
title: String,
message: String,
diagnostics: &mut Vec<Diagnostic>,
) {
let diagnostic = match hints {
"error" => Diagnostic::error(title, message),
"warning" => Diagnostic::warn(title, message),
_ => Diagnostic::error(title, format!("Invalid hints type: {hints}")),
};
diagnostics.push(diagnostic);
}

fn add_assets_over_size_limit_warning(
detail: &[(&String, f64)],
limit: f64,
hints: &str,
diagnostics: &mut Vec<Diagnostic>,
) {
let asset_list: String = detail
.iter()
.map(|&(name, size)| format!("\n {} ({})", name, format_size(size)))
.collect::<Vec<String>>()
.join("");
let title = String::from("assets over size limit warning");
let message = format!("asset size limit: The following asset(s) exceed the recommended size limit ({}). This can impact web performance.\nAssets:{}", format_size(limit), asset_list);

Self::add_diagnostic(hints, title, message, diagnostics);
}

fn add_entrypoints_over_size_limit_warning(
detail: &[(&String, f64, Vec<String>)],
limit: f64,
hints: &str,
diagnostics: &mut Vec<Diagnostic>,
) {
let entrypoint_list: String = detail
.iter()
.map(|(name, size, files)| {
format!(
"\n {} ({})\n{}",
name,
format_size(*size),
files
.iter()
.map(|file| format!(" {}", file))
.collect::<Vec<_>>()
.join("\n")
)
})
.collect::<Vec<_>>()
.join("");
let title = String::from("entrypoints over size limit warning");
let message = format!(
"entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit ({}). This can impact web performance.\nEntrypoints:{}",
format_size(limit),
entrypoint_list
);

Self::add_diagnostic(hints, title, message, diagnostics);
}
}

#[plugin_hook(CompilerAfterEmit for SizeLimitsPlugin)]
async fn after_emit(&self, compilation: &mut Compilation) -> Result<()> {
let hints = &self.options.hints;
let max_asset_size = self.options.max_asset_size.unwrap_or(250000.0);
let max_entrypoint_size = self.options.max_entrypoint_size.unwrap_or(250000.0);

let mut assets_over_size_limit = vec![];

for (name, asset) in compilation.assets() {
let source = asset.get_source();

if !self.asset_filter(name, asset).await {
continue;
}

if let Some(source) = source {
let size = source.size() as f64;

if size > max_asset_size {
assets_over_size_limit.push((name, size));
}
}
}

let mut entrypoints_over_limit = vec![];

for (name, ukey) in compilation.entrypoints.iter() {
let entry = compilation.chunk_group_by_ukey.expect_get(ukey);
let size = self.get_entrypoint_size(entry, compilation).await;

if size > max_entrypoint_size {
let mut files = vec![];

for filename in entry.get_files(&compilation.chunk_by_ukey) {
let asset = compilation.assets().get(&filename);

if let Some(asset) = asset {
if self.asset_filter(&filename, asset).await {
files.push(filename);
}
}
}

entrypoints_over_limit.push((name, size, files));
}
}

if let Some(hints) = hints {
let mut diagnostics = vec![];

if !assets_over_size_limit.is_empty() {
Self::add_assets_over_size_limit_warning(
&assets_over_size_limit,
max_asset_size,
hints,
&mut diagnostics,
);
}

if !entrypoints_over_limit.is_empty() {
Self::add_entrypoints_over_size_limit_warning(
&entrypoints_over_limit,
max_asset_size,
hints,
&mut diagnostics,
);
}

if !diagnostics.is_empty() {
let has_async_chunk = compilation
.chunk_by_ukey
.values()
.any(|chunk| !chunk.can_be_initial(&compilation.chunk_group_by_ukey));

if !has_async_chunk {
let title = String::from("no async chunks warning");
let message = String::from("Rspack performance recommendations:\nYou can limit the size of your bundles by using import() to lazy load some parts of your application.\nFor more info visit https://www.rspack.dev/guide/optimization/code-splitting");

Self::add_diagnostic(hints, title, message, &mut diagnostics);
}

compilation.push_batch_diagnostic(diagnostics);
}
}

Ok(())
}

impl Plugin for SizeLimitsPlugin {
fn name(&self) -> &'static str {
"SizeLimitsPlugin"
}

fn apply(
&self,
ctx: PluginContext<&mut ApplyContext>,
_options: &mut CompilerOptions,
) -> Result<()> {
ctx
.context
.compiler_hooks
.after_emit
.tap(after_emit::new(self));

Ok(())
}
}
Loading
Loading