feat(minifier): loop compressor passes (#6013)

This commit is contained in:
Boshen 2024-09-24 03:09:35 +00:00
parent 18371dda3d
commit 5c323a2105
13 changed files with 212 additions and 128 deletions

View file

@ -27,8 +27,9 @@ fn main() -> std::io::Result<()> {
println!("{printed}");
if twice {
let printed = minify(&printed, source_type, mangle);
println!("{printed}");
let printed2 = minify(&printed, source_type, mangle);
println!("{printed2}");
println!("same = {}", printed == printed2);
}
Ok(())

View file

@ -9,25 +9,37 @@ use crate::{CompressOptions, CompressorPass};
/// `var a; var b = 1; var c = 2` => `var a, b = 1; c = 2`
pub struct CollapseVariableDeclarations {
options: CompressOptions,
changed: bool,
}
impl<'a> CompressorPass<'a> for CollapseVariableDeclarations {}
impl<'a> CompressorPass<'a> for CollapseVariableDeclarations {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for CollapseVariableDeclarations {
fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
if self.options.join_vars {
self.join_vars(stmts, ctx);
}
self.join_vars(stmts, ctx);
}
}
impl<'a> CollapseVariableDeclarations {
pub fn new(options: CompressOptions) -> Self {
Self { options }
Self { options, changed: false }
}
/// Join consecutive var statements
fn join_vars(&self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
fn join_vars(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
if self.options.join_vars {
return;
}
// Collect all the consecutive ranges that contain joinable vars.
// This is required because Rust prevents in-place vec mutation.
let mut ranges = vec![];
@ -83,6 +95,7 @@ impl<'a> CollapseVariableDeclarations {
}
}
*stmts = new_stmts;
self.changed = true;
}
}

View file

@ -1,19 +1,31 @@
use oxc_traverse::Traverse;
use oxc_ast::ast::*;
use oxc_traverse::{Traverse, TraverseCtx};
use crate::CompressorPass;
/// Tries to chain assignments together.
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/ExploitAssigns.java>
pub struct ExploitAssigns;
pub struct ExploitAssigns {
changed: bool,
}
impl<'a> CompressorPass<'a> for ExploitAssigns {}
impl<'a> CompressorPass<'a> for ExploitAssigns {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for ExploitAssigns {}
impl ExploitAssigns {
pub fn new() -> Self {
Self {}
Self { changed: false }
}
}

View file

@ -18,7 +18,7 @@ pub use statement_fusion::StatementFusion;
use oxc_ast::ast::Program;
use oxc_semantic::{ScopeTree, SymbolTable};
use oxc_traverse::{walk_program, Traverse, TraverseCtx};
use oxc_traverse::{Traverse, TraverseCtx};
use crate::node_util::NodeUtil;
@ -33,11 +33,7 @@ impl<'a> NodeUtil for TraverseCtx<'a> {
}
pub trait CompressorPass<'a>: Traverse<'a> {
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>)
where
Self: Traverse<'a>,
Self: Sized,
{
walk_program(self, program, ctx);
}
fn changed(&self) -> bool;
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>);
}

View file

@ -22,10 +22,19 @@ use crate::{
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PeepholeFoldConstants.java>
pub struct PeepholeFoldConstants {
evaluate: bool,
changed: bool,
}
impl<'a> CompressorPass<'a> for PeepholeFoldConstants {}
impl<'a> CompressorPass<'a> for PeepholeFoldConstants {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for PeepholeFoldConstants {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
@ -35,17 +44,12 @@ impl<'a> Traverse<'a> for PeepholeFoldConstants {
impl<'a> PeepholeFoldConstants {
pub fn new() -> Self {
Self { evaluate: false }
}
pub fn with_evaluate(mut self, yes: bool) -> Self {
self.evaluate = yes;
self
Self { changed: false }
}
// [optimizeSubtree](https://github.com/google/closure-compiler/blob/75335a5138dde05030747abfd3c852cd34ea7429/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L72)
// TODO: tryReduceOperandsForOp
pub fn fold_expression(&self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
pub fn fold_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(folded_expr) = match expr {
Expression::BinaryExpression(e) => self.try_fold_binary_operator(e, ctx),
Expression::LogicalExpression(e)
@ -60,6 +64,7 @@ impl<'a> PeepholeFoldConstants {
_ => None,
} {
*expr = folded_expr;
self.changed = true;
};
}
@ -92,13 +97,7 @@ impl<'a> PeepholeFoldConstants {
&binary_expr.right,
ctx,
),
// NOTE: string concat folding breaks our current evaluation of Test262 tests. The
// minifier is tested by comparing output of running the minifier once and twice,
// respectively. Since Test262Error messages include string concats, the outputs
// don't match (even though the produced code is valid). Additionally, We'll likely
// want to add `evaluate` checks for all constant folding, not just additions, but
// we're adding this here until a decision is made.
BinaryOperator::Addition if self.evaluate => {
BinaryOperator::Addition => {
self.try_fold_addition(binary_expr.span, &binary_expr.left, &binary_expr.right, ctx)
}
_ => None,

View file

@ -10,9 +10,20 @@ use crate::CompressorPass;
/// with `? :` and short-circuit binary operators.
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PeepholeMinimizeConditions.java>
pub struct PeepholeMinimizeConditions;
pub struct PeepholeMinimizeConditions {
changed: bool,
}
impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions {}
impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for PeepholeMinimizeConditions {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
@ -21,13 +32,14 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions {
_ => None,
} {
*expr = folded_expr;
self.changed = true;
};
}
}
impl<'a> PeepholeMinimizeConditions {
pub fn new() -> Self {
Self
Self { changed: false }
}
/// Try to minimize NOT nodes such as `!(x==y)`.

View file

@ -10,17 +10,30 @@ use crate::{keep_var::KeepVar, node_util::NodeUtil, tri::Tri, CompressorPass};
/// Terser option: `dead_code: true`.
///
/// See `KeepVar` at the end of this file for `var` hoisting logic.
pub struct PeepholeRemoveDeadCode;
pub struct PeepholeRemoveDeadCode {
changed: bool,
}
impl<'a> CompressorPass<'a> for PeepholeRemoveDeadCode {}
impl<'a> CompressorPass<'a> for PeepholeRemoveDeadCode {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for PeepholeRemoveDeadCode {
fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fold_if_statement(stmt, ctx);
self.fold_if_statement(stmt, ctx);
}
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
if stmts.iter().any(|stmt| matches!(stmt, Statement::EmptyStatement(_))) {
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
}
self.dead_code_elimination(stmts, ctx);
}
@ -30,13 +43,14 @@ impl<'a> Traverse<'a> for PeepholeRemoveDeadCode {
_ => None,
} {
*expr = folded_expr;
self.changed = true;
}
}
}
impl<'a> PeepholeRemoveDeadCode {
pub fn new() -> Self {
Self {}
Self { changed: false }
}
/// Removes dead code thats comes after `return` statements after inlining `if` statements
@ -76,6 +90,7 @@ impl<'a> PeepholeRemoveDeadCode {
}
let mut i = 0;
let len = stmts.len();
stmts.retain(|s| {
i += 1;
if i - 1 <= index {
@ -88,25 +103,35 @@ impl<'a> PeepholeRemoveDeadCode {
false
});
let all_hoisted = keep_var.all_hoisted();
if let Some(stmt) = keep_var.get_variable_declaration_statement() {
stmts.push(stmt);
if !all_hoisted {
self.changed = true;
}
}
if stmts.len() != len {
self.changed = true;
}
}
fn fold_if_statement(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
fn fold_if_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::IfStatement(if_stmt) = stmt else { return };
// Descend and remove `else` blocks first.
if let Some(alternate) = &mut if_stmt.alternate {
Self::fold_if_statement(alternate, ctx);
self.fold_if_statement(alternate, ctx);
if matches!(alternate, Statement::EmptyStatement(_)) {
if_stmt.alternate = None;
self.changed = true;
}
}
match ctx.get_boolean_value(&if_stmt.test) {
Tri::True => {
*stmt = ctx.ast.move_statement(&mut if_stmt.consequent);
self.changed = true;
}
Tri::False => {
*stmt = if let Some(alternate) = &mut if_stmt.alternate {
@ -119,6 +144,7 @@ impl<'a> PeepholeRemoveDeadCode {
.get_variable_declaration_statement()
.unwrap_or_else(|| ctx.ast.statement_empty(SPAN))
};
self.changed = true;
}
Tri::Unknown => {}
}

View file

@ -14,9 +14,19 @@ use crate::{node_util::NodeUtil, CompressOptions, CompressorPass};
pub struct PeepholeSubstituteAlternateSyntax {
options: CompressOptions,
in_define_export: bool,
changed: bool,
}
impl<'a> CompressorPass<'a> for PeepholeSubstituteAlternateSyntax {}
impl<'a> CompressorPass<'a> for PeepholeSubstituteAlternateSyntax {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
fn enter_statement(&mut self, stmt: &mut Statement<'a>, _ctx: &mut TraverseCtx<'a>) {
@ -30,7 +40,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
_ctx: &mut TraverseCtx<'a>,
) {
// We may fold `void 1` to `void 0`, so compress it after visiting
Self::compress_return_statement(stmt);
self.compress_return_statement(stmt);
}
fn enter_variable_declaration(
@ -39,7 +49,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
_ctx: &mut TraverseCtx<'a>,
) {
for declarator in decl.declarations.iter_mut() {
Self::compress_variable_declarator(declarator);
self.compress_variable_declarator(declarator);
}
}
@ -83,7 +93,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
impl<'a> PeepholeSubstituteAlternateSyntax {
pub fn new(options: CompressOptions) -> Self {
Self { options, in_define_export: false }
Self { options, in_define_export: false, changed: false }
}
/* Utilities */
@ -120,14 +130,14 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
/// Remove block from single line blocks
/// `{ block } -> block`
#[allow(clippy::only_used_in_recursion)] // `&self` is only used in recursion
fn compress_block(&self, stmt: &mut Statement<'a>) {
fn compress_block(&mut self, stmt: &mut Statement<'a>) {
if let Statement::BlockStatement(block) = stmt {
// Avoid compressing `if (x) { var x = 1 }` to `if (x) var x = 1` due to different
// semantics according to AnnexB, which lead to different semantics.
if block.body.len() == 1 && !block.body[0].is_declaration() {
*stmt = block.body.remove(0);
self.compress_block(stmt);
self.changed = true;
}
}
}
@ -230,18 +240,20 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
///
/// `return undefined` -> `return`
/// `return void 0` -> `return`
fn compress_return_statement(stmt: &mut ReturnStatement<'a>) {
fn compress_return_statement(&mut self, stmt: &mut ReturnStatement<'a>) {
if stmt.argument.as_ref().is_some_and(|expr| expr.is_undefined() || expr.is_void_0()) {
stmt.argument = None;
self.changed = true;
}
}
fn compress_variable_declarator(decl: &mut VariableDeclarator<'a>) {
fn compress_variable_declarator(&mut self, decl: &mut VariableDeclarator<'a>) {
if decl.kind.is_const() {
return;
}
if decl.init.as_ref().is_some_and(|init| init.is_undefined() || init.is_void_0()) {
decl.init = None;
self.changed = true;
}
}
}

View file

@ -13,7 +13,15 @@ pub struct RemoveSyntax {
options: CompressOptions,
}
impl<'a> CompressorPass<'a> for RemoveSyntax {}
impl<'a> CompressorPass<'a> for RemoveSyntax {
fn changed(&self) -> bool {
false
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for RemoveSyntax {
fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) {

View file

@ -10,32 +10,43 @@ use crate::CompressorPass;
/// Tries to fuse all the statements in a block into a one statement by using COMMAs or statements.
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/StatementFusion.java>
pub struct StatementFusion;
pub struct StatementFusion {
changed: bool,
}
impl<'a> CompressorPass<'a> for StatementFusion {}
impl<'a> CompressorPass<'a> for StatementFusion {
fn changed(&self) -> bool {
self.changed
}
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
}
}
impl<'a> Traverse<'a> for StatementFusion {
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fuse_statements(&mut program.body, ctx);
self.fuse_statements(&mut program.body, ctx);
}
fn exit_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fuse_statements(&mut body.statements, ctx);
self.fuse_statements(&mut body.statements, ctx);
}
fn exit_block_statement(&mut self, block: &mut BlockStatement<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fuse_statements(&mut block.body, ctx);
self.fuse_statements(&mut block.body, ctx);
}
}
impl<'a> StatementFusion {
pub fn new() -> Self {
Self {}
Self { changed: false }
}
fn fuse_statements(stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
fn fuse_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
if Self::can_fuse_into_one_statement(stmts) {
Self::fuse_into_one_statement(stmts, ctx);
self.fuse_into_one_statement(stmts, ctx);
}
}
@ -75,7 +86,11 @@ impl<'a> StatementFusion {
}
}
fn fuse_into_one_statement(stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
fn fuse_into_one_statement(
&mut self,
stmts: &mut Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
let len = stmts.len();
let mut expressions = ctx.ast.vec();
@ -103,6 +118,7 @@ impl<'a> StatementFusion {
Self::fuse_expression_into_control_flow_statement(last, expressions, ctx);
*stmts = ctx.ast.vec1(ctx.ast.move_statement(last));
self.changed = true;
}
fn fuse_expression_into_control_flow_statement(

View file

@ -35,69 +35,50 @@ impl<'a> Compressor<'a> {
program: &mut Program<'a>,
) {
let mut ctx = TraverseCtx::new(scopes, symbols, self.allocator);
self.remove_syntax(program, &mut ctx);
RemoveSyntax::new(self.options).build(program, &mut ctx);
if self.options.dead_code_elimination {
self.dead_code_elimination(program, &mut ctx);
return;
}
// earlyPeepholeOptimizations
// TODO: MinimizeExitPoints
self.minimize_conditions(program, &mut ctx);
self.substitute_alternate_syntax(program, &mut ctx);
// TODO: PeepholeReplaceKnownMethods
self.remove_dead_code(program, &mut ctx);
self.fold_constants(program, &mut ctx);
ExploitAssigns::new().build(program, &mut ctx);
CollapseVariableDeclarations::new(self.options).build(program, &mut ctx);
// latePeepholeOptimizations
// TODO: StatementFusion
self.remove_dead_code(program, &mut ctx);
self.minimize_conditions(program, &mut ctx);
self.substitute_alternate_syntax(program, &mut ctx);
// TODO: PeepholeReplaceKnownMethods
self.fold_constants(program, &mut ctx);
// See `latePeepholeOptimizations`
let mut passes: [&mut dyn CompressorPass; 5] = [
&mut StatementFusion::new(),
&mut PeepholeRemoveDeadCode::new(),
// TODO: MinimizeExitPoints
&mut PeepholeMinimizeConditions::new(),
&mut PeepholeSubstituteAlternateSyntax::new(self.options),
// TODO: PeepholeReplaceKnownMethods
&mut PeepholeFoldConstants::new(),
];
self.exploit_assigns(program, &mut ctx);
self.collapse_variable_declarations(program, &mut ctx);
let mut i = 0;
loop {
let mut changed = false;
for pass in &mut passes {
pass.build(program, &mut ctx);
if pass.changed() {
changed = true;
}
}
if !changed {
break;
}
if i > 50 {
debug_assert!(false, "Ran in a infinite loop.");
break;
}
i += 1;
}
}
fn dead_code_elimination(self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.fold_constants(program, ctx);
self.minimize_conditions(program, ctx);
self.remove_dead_code(program, ctx);
}
fn remove_syntax(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
RemoveSyntax::new(self.options).build(program, ctx);
}
fn minimize_conditions(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
PeepholeFoldConstants::new().build(program, ctx);
PeepholeMinimizeConditions::new().build(program, ctx);
}
fn fold_constants(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
PeepholeFoldConstants::new().with_evaluate(self.options.evaluate).build(program, ctx);
}
fn substitute_alternate_syntax(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
PeepholeSubstituteAlternateSyntax::new(self.options).build(program, ctx);
}
fn remove_dead_code(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
PeepholeRemoveDeadCode::new().build(program, ctx);
}
#[allow(unused)]
fn statement_fusion(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
StatementFusion::new().build(program, ctx);
}
fn collapse_variable_declarations(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
CollapseVariableDeclarations::new(self.options).build(program, ctx);
}
fn exploit_assigns(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
ExploitAssigns::new().build(program, ctx);
}
}

View file

@ -4,6 +4,7 @@ use oxc_span::{Atom, Span, SPAN};
pub struct KeepVar<'a> {
ast: AstBuilder<'a>,
vars: std::vec::Vec<(Atom<'a>, Span)>,
all_hoisted: bool,
}
impl<'a> Visit<'a> for KeepVar<'a> {
@ -37,6 +38,9 @@ impl<'a> Visit<'a> for KeepVar<'a> {
decl.bound_names(&mut |ident| {
self.vars.push((ident.name.clone(), ident.span));
});
if decl.has_init() {
self.all_hoisted = false;
}
}
}
_ => {}
@ -46,7 +50,11 @@ impl<'a> Visit<'a> for KeepVar<'a> {
impl<'a> KeepVar<'a> {
pub fn new(ast: AstBuilder<'a>) -> Self {
Self { ast, vars: std::vec![] }
Self { ast, vars: std::vec![], all_hoisted: true }
}
pub fn all_hoisted(&self) -> bool {
self.all_hoisted
}
pub fn get_variable_declaration_statement(self) -> Option<Statement<'a>> {

View file

@ -1,26 +1,26 @@
Original | Minified | esbuild | Gzip | esbuild
72.14 kB | 24.32 kB | 23.70 kB | 8.71 kB | 8.54 kB | react.development.js
72.14 kB | 24.47 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js
173.90 kB | 61.79 kB | 59.82 kB | 19.57 kB | 19.33 kB | moment.js
173.90 kB | 61.71 kB | 59.82 kB | 19.56 kB | 19.33 kB | moment.js
287.63 kB | 92.89 kB | 90.07 kB | 32.32 kB | 31.95 kB | jquery.js
287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js
342.15 kB | 122.41 kB | 118.14 kB | 44.77 kB | 44.37 kB | vue.js
342.15 kB | 124.14 kB | 118.14 kB | 44.81 kB | 44.37 kB | vue.js
544.10 kB | 73.54 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js
544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js
555.77 kB | 277.01 kB | 270.13 kB | 91.22 kB | 90.80 kB | d3.js
555.77 kB | 278.71 kB | 270.13 kB | 91.40 kB | 90.80 kB | d3.js
1.01 MB | 468.10 kB | 458.89 kB | 126.67 kB | 126.71 kB | bundle.min.js
1.01 MB | 470.11 kB | 458.89 kB | 126.97 kB | 126.71 kB | bundle.min.js
1.25 MB | 663.51 kB | 646.76 kB | 163.76 kB | 163.73 kB | three.js
1.25 MB | 671.02 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js
2.14 MB | 742.01 kB | 724.14 kB | 181.59 kB | 181.07 kB | victory.js
2.14 MB | 756.70 kB | 724.14 kB | 182.87 kB | 181.07 kB | victory.js
3.20 MB | 1.03 MB | 1.01 MB | 332.45 kB | 331.56 kB | echarts.js
3.20 MB | 1.05 MB | 1.01 MB | 334.11 kB | 331.56 kB | echarts.js
6.69 MB | 2.39 MB | 2.31 MB | 496.45 kB | 488.28 kB | antd.js
6.69 MB | 2.44 MB | 2.31 MB | 498.90 kB | 488.28 kB | antd.js
10.95 MB | 3.56 MB | 3.49 MB | 911.22 kB | 915.50 kB | typescript.js
10.95 MB | 3.59 MB | 3.49 MB | 913.91 kB | 915.50 kB | typescript.js