Skip to content

Commit

Permalink
minify symbols in each chunk separately (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Aug 11, 2020
1 parent bf0a802 commit 940e992
Show file tree
Hide file tree
Showing 19 changed files with 967 additions and 779 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

* Symbols are now renamed separately per chunk ([#16](https://github.com/evanw/esbuild/issues/16))

Previously, bundling with code splitting assigned minified names using a single frequency distribution calculated across all chunks. This meant that typical code changes in one chunk would often cause the contents of all chunks to change, which negated some of the benefits of the browser cache.

Now symbol renaming (both minified and not minified) is done separately per chunk. It was challenging to implement this without making esbuild a lot slower and causing it to use a lot more memory. Symbol renaming has been mostly rewritten to accomplish this and appears to actually usually use a little less memory and run a bit faster than before, even for code splitting builds that generate a lot of chunks. In addition, minified chunks are now slightly smaller because a given minified name can now be reused by multiple chunks.

## 0.6.19

* Reduce memory usage for large builds by 30-40% ([#304](https://github.com/evanw/esbuild/issues/304))
Expand Down
80 changes: 68 additions & 12 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,9 @@ const (
SymbolPrivateStaticSet
SymbolPrivateStaticGetSetPair

// Labels are in their own namespace
SymbolLabel

// TypeScript enums can merge with TypeScript namespaces and other TypeScript
// enums.
SymbolTSEnum
Expand Down Expand Up @@ -1100,9 +1103,12 @@ const (
ImportItemMissing
)

// Note: the order of valies in this struct matters to reduce struct size.
// Note: the order of values in this struct matters to reduce struct size.
type Symbol struct {
Name string
// This is the name that came from the parser. Printed names may be renamed
// during minification or to avoid name collisions. Do not use the original
// name during printing.
OriginalName string

// This is used for symbols that represent items in the import clause of an
// ES6 import statement. These should always be referenced by EImportIdentifier
Expand All @@ -1123,17 +1129,31 @@ type Symbol struct {
// FollowSymbols to get the real one.
Link Ref

// An estimate of the number of uses of this symbol. This is used for
// minification (to prefer shorter names for more frequently used symbols).
// The reason why this is an estimate instead of an accurate count is that
// it's not updated during dead code elimination for speed. I figure that
// even without updating after parsing it's still a pretty good heuristic.
// An estimate of the number of uses of this symbol. This is used to detect
// whether a symbol is used or not. For example, TypeScript imports that are
// unused must be removed because they are probably type-only imports. This
// is an estimate and may not be completely accurate due to oversights in the
// code. But it should always be non-zero when the symbol is used.
UseCountEstimate uint32

// This is for code splitting. Stored as one's complement so the zero value
// is invalid.
// This is for generating cross-chunk imports and exports for code splitting.
// It's stored as one's complement so the zero value is invalid.
ChunkIndex uint32

// This is used for minification. Symbols that are declared in sibling scopes
// can share a name. A good heuristic (from Google Closure Compiler) is to
// assign names to symbols from sibling scopes in declaration order. That way
// local variable names are reused in each global function like this, which
// improves gzip compression:
//
// function x(a, b) { ... }
// function y(a, b, c) { ... }
//
// The parser fills this in for symbols inside nested scopes. There are three
// slot namespaces: regular symbols, label symbols, and private symbols. This
// is stored as one's complement so the zero value is invalid.
NestedScopeSlot uint32

Kind SymbolKind

// Certain symbols must not be renamed or minified. For example, the
Expand Down Expand Up @@ -1161,6 +1181,40 @@ type Symbol struct {
ImportItemStatus ImportItemStatus
}

type SlotNamespace uint8

const (
SlotDefault SlotNamespace = iota
SlotLabel
SlotPrivateName
SlotMustNotBeRenamed
)

func (s *Symbol) SlotNamespace() SlotNamespace {
if s.Kind == SymbolUnbound || s.MustNotBeRenamed {
return SlotMustNotBeRenamed
}
if s.Kind.IsPrivate() {
return SlotPrivateName
}
if s.Kind == SymbolLabel {
return SlotLabel
}
return SlotDefault
}

type SlotCounts [3]uint32

func (a *SlotCounts) UnionMax(b SlotCounts) {
for i := range *a {
ai := &(*a)[i]
bi := b[i]
if *ai < bi {
*ai = bi
}
}
}

type NamespaceAlias struct {
NamespaceRef Ref
Alias string
Expand Down Expand Up @@ -1265,8 +1319,9 @@ type ImportRecord struct {
}

type AST struct {
ApproximateLineCount int32
HasLazyExport bool
ApproximateLineCount int32
NestedScopeSlotCounts SlotCounts
HasLazyExport bool

// This is a list of CommonJS features. When a file uses CommonJS features,
// it's not a candidate for "flat bundling" and must be wrapped in its own
Expand Down Expand Up @@ -1336,7 +1391,8 @@ type NamedImport struct {
// shaking and can be assigned to separate chunks (i.e. output files) by code
// splitting.
type Part struct {
Stmts []Stmt
Stmts []Stmt
Scopes []*Scope

// Each is an index into the file-level import record list
ImportRecordIndices []uint32
Expand Down
21 changes: 12 additions & 9 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -852,9 +852,10 @@ func (b *Bundle) generateMetadataJSON(results []OutputFile) []byte {
}

type runtimeCacheKey struct {
MangleSyntax bool
ES6 bool
Platform config.Platform
MangleSyntax bool
MinifyIdentifiers bool
ES6 bool
Platform config.Platform
}

type runtimeCache struct {
Expand All @@ -870,9 +871,10 @@ var globalRuntimeCache runtimeCache
func (cache *runtimeCache) parseRuntime(options *config.Options) (source logging.Source, runtimeAST ast.AST, ok bool) {
key := runtimeCacheKey{
// All configuration options that the runtime code depends on must go here
MangleSyntax: options.MangleSyntax,
Platform: options.Platform,
ES6: runtime.CanUseES6(options.UnsupportedFeatures),
MangleSyntax: options.MangleSyntax,
MinifyIdentifiers: options.MinifyIdentifiers,
Platform: options.Platform,
ES6: runtime.CanUseES6(options.UnsupportedFeatures),
}

// Determine which source to use
Expand All @@ -898,9 +900,10 @@ func (cache *runtimeCache) parseRuntime(options *config.Options) (source logging
log := logging.NewDeferLog()
runtimeAST, ok = parser.Parse(log, source, config.Options{
// These configuration options must only depend on the key
MangleSyntax: key.MangleSyntax,
Platform: key.Platform,
Defines: cache.processedDefines(key.Platform),
MangleSyntax: key.MangleSyntax,
MinifyIdentifiers: key.MinifyIdentifiers,
Platform: key.Platform,
Defines: cache.processedDefines(key.Platform),

// Always do tree shaking for the runtime because we never want to
// include unnecessary runtime code
Expand Down
Loading

0 comments on commit 940e992

Please sign in to comment.