Skip to content

Commit 8b772e2

Browse files
committed
Under Minify, assign prototypes temporarily through $p.
When minifying, a susprisingly large amount of bytes in the resulting .js file are caused by the `prototype`s in: C.prototype.f = function(...) { ... }; C.prototype.g = function(...) { ... }; We can get rid of them by assigning `C.prototype` once to a temporary variable, then reusing it many times: $p = C.prototype; $p.f = function(...) { ... }; $p.f = function(...) { ... }; This commit implements that strategy when the `minify` config is on.
1 parent 8dd02c7 commit 8b772e2

File tree

7 files changed

+83
-30
lines changed

7 files changed

+83
-30
lines changed

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) {
186186

187187
val chainProtoWithGlobals = superClass match {
188188
case None =>
189-
WithGlobals.nil
189+
WithGlobals(setPrototypeVar(ctorVar))
190190

191191
case Some(_) if shouldExtendJSError(className, superClass) =>
192-
globalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _))
192+
globalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _, localDeclPrototypeVar = false))
193193

194194
case Some(parentIdent) =>
195-
WithGlobals(List(ctorVar.prototype := js.New(globalVar(VarField.h, parentIdent.name), Nil)))
195+
WithGlobals(List(genAssignPrototype(ctorVar, js.New(globalVar(VarField.h, parentIdent.name), Nil))))
196196
}
197197

198198
for {
@@ -208,12 +208,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) {
208208
js.JSDocConstructor(realCtorDef.head) ::
209209
realCtorDef.tail :::
210210
chainProto :::
211-
(genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) ::
211+
(genIdentBracketSelect(prototypeFor(ctorVar), "constructor") := ctorVar) ::
212212

213213
// Inheritable constructor
214214
js.JSDocConstructor(inheritableCtorDef.head) ::
215215
inheritableCtorDef.tail :::
216-
(globalVar(VarField.h, className).prototype := ctorVar.prototype) :: Nil
216+
(globalVar(VarField.h, className).prototype := prototypeFor(ctorVar)) :: Nil
217217
)
218218
}
219219
}
@@ -243,8 +243,8 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) {
243243
val ctorVar = fileLevelVar(VarField.b, genName(className))
244244

245245
js.JSDocConstructor(ctorVar := ctorFun) ::
246-
chainPrototypeWithLocalCtor(className, ctorVar, superCtor) :::
247-
(genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: Nil
246+
chainPrototypeWithLocalCtor(className, ctorVar, superCtor, localDeclPrototypeVar = true) :::
247+
(genIdentBracketSelect(prototypeFor(ctorVar), "constructor") := ctorVar) :: Nil
248248
}
249249
}
250250
}
@@ -333,15 +333,15 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) {
333333
}
334334

335335
private def chainPrototypeWithLocalCtor(className: ClassName, ctorVar: js.Tree,
336-
superCtor: js.Tree)(implicit pos: Position): List[js.Tree] = {
336+
superCtor: js.Tree, localDeclPrototypeVar: Boolean)(implicit pos: Position): List[js.Tree] = {
337337
import TreeDSL._
338338

339339
val dummyCtor = fileLevelVar(VarField.hh, genName(className))
340340

341341
List(
342342
js.JSDocConstructor(genConst(dummyCtor.ident, js.Function(false, Nil, None, js.Skip()))),
343343
dummyCtor.prototype := superCtor.prototype,
344-
ctorVar.prototype := js.New(dummyCtor, Nil)
344+
genAssignPrototype(ctorVar, js.New(dummyCtor, Nil), localDeclPrototypeVar)
345345
)
346346
}
347347

@@ -638,7 +638,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) {
638638
else globalVar(VarField.c, className)
639639

640640
if (namespace.isStatic) classVarRef
641-
else classVarRef.prototype
641+
else prototypeFor(classVarRef)
642642
}
643643

644644
def genMemberNameTree(name: Tree)(

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ private[emitter] object CoreJSLib {
562562
extractWithGlobals(globalClassDef(VarField.Char, CoreVar, None, ctor :: toStr :: Nil))
563563
} else {
564564
defineFunction(VarField.Char, ctor.args, ctor.body) :::
565+
setPrototypeVar(globalVar(VarField.Char, CoreVar)) :::
565566
assignES5ClassMembers(globalVar(VarField.Char, CoreVar), List(toStr))
566567
}
567568
}
@@ -1528,16 +1529,16 @@ private[emitter] object CoreJSLib {
15281529
val clsDef = {
15291530
extractWithGlobals(globalFunctionDef(VarField.ac, componentTypeRef,
15301531
ctor.args, ctor.restParam, ctor.body)) :::
1531-
(ArrayClass.prototype := New(globalVar(VarField.h, ObjectClass), Nil)) ::
1532-
(ArrayClass.prototype DOT "constructor" := ArrayClass) ::
1532+
genAssignPrototype(ArrayClass, New(globalVar(VarField.h, ObjectClass), Nil)) ::
1533+
(prototypeFor(ArrayClass) DOT "constructor" := ArrayClass) ::
15331534
assignES5ClassMembers(ArrayClass, members)
15341535
}
15351536

15361537
componentTypeRef match {
15371538
case _: ClassRef =>
15381539
clsDef :::
15391540
extractWithGlobals(globalFunctionDef(VarField.ah, ObjectClass, Nil, None, Skip())) :::
1540-
(globalVar(VarField.ah, ObjectClass).prototype := ArrayClass.prototype) :: Nil
1541+
(globalVar(VarField.ah, ObjectClass).prototype := prototypeFor(ArrayClass)) :: Nil
15411542
case _: PrimRef =>
15421543
clsDef
15431544
}
@@ -1697,7 +1698,6 @@ private[emitter] object CoreJSLib {
16971698
val name = varRef("name")
16981699

16991700
Block(
1700-
arrayClass.prototype DOT classData := This(),
17011701
const(name, str("[") + (componentData DOT cpn.arrayEncodedName)),
17021702
privateFieldSet(cpn.constr, arrayClass),
17031703
if (globalKnowledge.isParentDataAccessed)
@@ -1729,6 +1729,7 @@ private[emitter] object CoreJSLib {
17291729
MethodDef(static = false, Ident(cpn.initSpecializedArray),
17301730
paramList(componentData, arrayClass, typedArrayClass, isAssignableFromFun), None, {
17311731
Block(
1732+
arrayClass.prototype DOT classData := This(),
17321733
initArrayCommonBody(arrayClass, componentData, componentData, 1),
17331734
const(self, This()), // capture `this` for use in arrow fun
17341735
privateFieldSet(cpn.isAssignableFromFun, isAssignableFromFun || {
@@ -1833,14 +1834,19 @@ private[emitter] object CoreJSLib {
18331834
val members = set ::: copyTo ::: clone :: Nil
18341835

18351836
if (useClassesForRegularClasses) {
1836-
ClassDef(Some(ArrayClass.ident), Some(globalVar(VarField.ac, ObjectClass)),
1837-
ctor :: members)
1837+
Block(
1838+
ClassDef(Some(ArrayClass.ident), Some(globalVar(VarField.ac, ObjectClass)),
1839+
ctor :: members),
1840+
ArrayClass.prototype DOT cpn.classData := This()
1841+
)
18381842
} else {
18391843
Block(
18401844
FunctionDef(ArrayClass.ident, ctor.args, ctor.restParam, ctor.body) ::
1841-
(ArrayClass.prototype := New(globalVar(VarField.ah, ObjectClass), Nil)) ::
1842-
(ArrayClass.prototype DOT "constructor" := ArrayClass) ::
1843-
assignES5ClassMembers(ArrayClass, members)
1845+
genAssignPrototype(ArrayClass, New(globalVar(VarField.ah, ObjectClass), Nil), localDecl = true) ::
1846+
(prototypeFor(ArrayClass) DOT "constructor" := ArrayClass) ::
1847+
assignES5ClassMembers(ArrayClass, members) :::
1848+
(prototypeFor(ArrayClass) DOT cpn.classData := This()) ::
1849+
Nil
18441850
)
18451851
}
18461852
}
@@ -2000,6 +2006,7 @@ private[emitter] object CoreJSLib {
20002006
extractWithGlobals(globalClassDef(VarField.TypeData, CoreVar, None, ctor :: members))
20012007
} else {
20022008
defineFunction(VarField.TypeData, ctor.args, ctor.body) :::
2009+
setPrototypeVar(globalVar(VarField.TypeData, CoreVar)) :::
20032010
assignES5ClassMembers(globalVar(VarField.TypeData, CoreVar), members)
20042011
}
20052012
}
@@ -2159,7 +2166,7 @@ private[emitter] object CoreJSLib {
21592166
for {
21602167
MethodDef(static, name, args, restParam, body) <- members
21612168
} yield {
2162-
val target = if (static) classRef else classRef.prototype
2169+
val target = if (static) classRef else prototypeFor(classRef)
21632170
genPropSelect(target, name) := Function(arrow = false, args, restParam, body)
21642171
}
21652172
}

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ final class Emitter[E >: Null <: js.Tree](
6464

6565
val classEmitter: ClassEmitter = new ClassEmitter(sjsGen)
6666

67+
val everyFileStart: List[E] = {
68+
// This postTransform does not count in the statistics
69+
postTransformer.transformStats(sjsGen.declarePrototypeVar, 0)
70+
}
71+
6772
val coreJSLibCache: CoreJSLibCache = new CoreJSLibCache
6873

6974
val moduleCaches: mutable.Map[ModuleID, ModuleCache] = mutable.Map.empty
@@ -293,6 +298,11 @@ final class Emitter[E >: Null <: js.Tree](
293298
* it is crucial that we verify it.
294299
*/
295300
val defTrees: List[E] = (
301+
/* The declaration of the `$p` variable that temporarily holds
302+
* prototypes.
303+
*/
304+
state.everyFileStart.iterator ++
305+
296306
/* The definitions of the CoreJSLib that come before the definition
297307
* of `j.l.Object`. They depend on nothing else.
298308
*/

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,39 @@ private[emitter] final class SJSGen(
145145
val newArrayOfThisClass = "newArrayOfThisClass"
146146
}
147147

148+
/* This is a `val` because it is used at the top of every file, outside of
149+
* any cache. Fortunately it does not depend on any dynamic content.
150+
*/
151+
val declarePrototypeVar: List[Tree] = {
152+
implicit val pos = Position.NoPosition
153+
if (minify) VarDef(fileLevelVarIdent(VarField.p), None) :: Nil
154+
else Nil
155+
}
156+
157+
def prototypeFor(classRef: Tree)(implicit pos: Position): Tree = {
158+
import TreeDSL._
159+
if (minify) fileLevelVar(VarField.p)
160+
else classRef.prototype
161+
}
162+
163+
def genAssignPrototype(classRef: Tree, value: Tree, localDecl: Boolean = false)(implicit pos: Position): Tree = {
164+
import TreeDSL._
165+
val assign = classRef.prototype := value
166+
if (!minify)
167+
assign
168+
else if (localDecl)
169+
VarDef(fileLevelVarIdent(VarField.p), Some(assign))
170+
else
171+
fileLevelVar(VarField.p) := assign
172+
}
173+
174+
/** Under `minify`, set `$p` to `classRef.prototype`. */
175+
def setPrototypeVar(classRef: Tree)(implicit pos: Position): List[Tree] = {
176+
import TreeDSL._
177+
if (minify) (fileLevelVar(VarField.p) := classRef.prototype) :: Nil
178+
else Nil
179+
}
180+
148181
def genZeroOf(tpe: Type)(
149182
implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
150183
pos: Position): Tree = {

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ private[emitter] object VarField {
4040
/** Scala class initializers (<clinit>). */
4141
final val sct = mk("$sct")
4242

43-
/** Private (instance) methods. */
43+
/** Private (instance) methods.
44+
*
45+
* Also used for the `prototype` of the current class when minifying.
46+
*/
4447
final val p = mk("$p")
4548

4649
/** Public static methods. */

linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class LibrarySizeTest {
7171

7272
testLinkedSizes(
7373
expectedFastLinkSize = 150205,
74-
expectedFullLinkSizeWithoutClosure = 92762,
74+
expectedFullLinkSizeWithoutClosure = 90108,
7575
expectedFullLinkSizeWithClosure = 21325,
7676
classDefs,
7777
moduleInitializers = MainTestModuleInitializers

project/Build.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,10 +2005,10 @@ object Build {
20052005
))
20062006
} else {
20072007
Some(ExpectedSizes(
2008-
fastLink = 495000 to 496000,
2009-
fullLink = 337000 to 338000,
2010-
fastLinkGz = 69000 to 70000,
2011-
fullLinkGz = 50000 to 51000,
2008+
fastLink = 454000 to 455000,
2009+
fullLink = 306000 to 307000,
2010+
fastLinkGz = 65000 to 66000,
2011+
fullLinkGz = 47000 to 48000,
20122012
))
20132013
}
20142014

@@ -2022,10 +2022,10 @@ object Build {
20222022
))
20232023
} else {
20242024
Some(ExpectedSizes(
2025-
fastLink = 348000 to 349000,
2026-
fullLink = 308000 to 309000,
2027-
fastLinkGz = 54000 to 55000,
2028-
fullLinkGz = 49000 to 50000,
2025+
fastLink = 325000 to 326000,
2026+
fullLink = 285000 to 286000,
2027+
fastLinkGz = 51000 to 52000,
2028+
fullLinkGz = 47000 to 48000,
20292029
))
20302030
}
20312031

0 commit comments

Comments
 (0)