summaryrefslogtreecommitdiff
path: root/book/src/main/scalatex/book/indepth
diff options
context:
space:
mode:
Diffstat (limited to 'book/src/main/scalatex/book/indepth')
-rw-r--r--book/src/main/scalatex/book/indepth/CompilationPipeline.scalatex26
-rw-r--r--book/src/main/scalatex/book/indepth/DesignSpace.scalatex30
-rw-r--r--book/src/main/scalatex/book/indepth/JavaAPIs.scalatex8
-rw-r--r--book/src/main/scalatex/book/indepth/SemanticDifferences.scalatex38
4 files changed, 51 insertions, 51 deletions
diff --git a/book/src/main/scalatex/book/indepth/CompilationPipeline.scalatex b/book/src/main/scalatex/book/indepth/CompilationPipeline.scalatex
index 7282ff1..f54efbf 100644
--- a/book/src/main/scalatex/book/indepth/CompilationPipeline.scalatex
+++ b/book/src/main/scalatex/book/indepth/CompilationPipeline.scalatex
@@ -15,7 +15,7 @@
Scala.js often is delivered to many users over the network, and long download times force users to wait, degrading the user experience
@p
- These factors combined means that Scala.js has to put in extra effort to optimize the code to reduce it's size at compile-time.
+ These factors combined means that Scala.js has to put in extra effort to optimize the code to reduce it's size at compile-time.
@sect{Raw Verbosity}
@p
@@ -40,26 +40,26 @@
Scala.js applications often run in the browser. Not just any browser, but the browsers of your users, who had come to your website or web-app to try and accomplish some task. This is in stark contrast the Scala-JVM applications, which most often run on servers: servers that you own and control, and can deploy code to at your leisure.
@p
- When running code on your own servers in some data center, you often do not care how big the compiled code is: the Scala standard library is several (6-7) megabytes, which added to your own code and any third-party libraries you're using, may add up to tens of megabytes, maybe a hundred or two if it's a relatively large application. Even that pales in comparison to the size of the JVM, which weighs in the 100s of megabytes.
+ When running code on your own servers in some data center, you often do not care how big the compiled code is: the Scala standard library is several (6-7) megabytes, which added to your own code and any third-party libraries you're using, may add up to tens of megabytes, maybe a hundred or two if it's a relatively large application. Even that pales in comparison to the size of the JVM, which weighs in the 100s of megabytes.
@p
Even so, you are deploying your code on an machine (virtual or real) which has several gigabytes of memory and 100s of gigabytes of disk space. Even if the size of the code makes deployment slower, you only deploy fresh code a handful of times a day at most, and the size of your executable typically does not worry you.
@p
- Scala.js is different: it runs in the browsers of your users. Before it can run in their browser, it first has to be downloaded, probably over a connection that is much slower than the one used to deploy your code to your servers or data-center. It probably is downloaded thousands of times per day, and every user which downloads it must pay the cost of waiting for it to finish downloading before they can take any actions on your website.
+ Scala.js is different: it runs in the browsers of your users. Before it can run in their browser, it first has to be downloaded, probably over a connection that is much slower than the one used to deploy your code to your servers or data-center. It probably is downloaded thousands of times per day, and every user which downloads it must pay the cost of waiting for it to finish downloading before they can take any actions on your website.
@p
A typical website loads ~100kb-1mb of Javascript, and 1mb is on the heavy side. Most Javascript libraries weigh in on the order of 50-100kb. For Scala.js to be useful in the browser, it has to be able to compare favorably with these numbers.
-
+
@hr
-
+
@p
Thus, while on Scala-JVM you typically have executables that (including dependencies) end up weighing 10s to 100s of megabytes, Scala.js has a much tighter budget. A hello world Scala.js application weighs in at around 100kb, and as you write more code and use more libraries (and parts of the standard library) this number rises to the 100s of kb. This isn't tiny, especially compared to the many small Javascript libraries out there, but it definitely is much smaller than what you'd be used to on the JVM.
@sect{Whole Program Optimizaton}
@p
- At a first approximation, Scala.js achieves its tiny executables by using whole-program optimization. Scala-JVM, like Java, allows for separate compilation: this means that after compilation, you can combine your compiled code with code compiled separately, which can interact with the code you already compiled in an ad-hoc basis: code from both sides can call each others methods, instantiate each others classes, etc. without any limits.
+ At a first approximation, Scala.js achieves its tiny executables by using whole-program optimization. Scala-JVM, like Java, allows for separate compilation: this means that after compilation, you can combine your compiled code with code compiled separately, which can interact with the code you already compiled in an ad-hoc basis: code from both sides can call each others methods, instantiate each others classes, etc. without any limits.
@p
- Even things like package-private do not help you: Java packages are separate-compile-able too, and multiple compilation runs can dump things in the same package! You may think that private members and methods may be some salvation, but the Java ecosystem typically relies heavily on reflection, which depends on the fact that these private things remain exactly as-they-are.
+ Even things like package-private do not help you: Java packages are separate-compile-able too, and multiple compilation runs can dump things in the same package! You may think that private members and methods may be some salvation, but the Java ecosystem typically relies heavily on reflection, which depends on the fact that these private things remain exactly as-they-are.
@p
Overall, this makes it difficult to do any meaningful optimization: you never know whether or not you can eliminate a class, method or field. Even if it's not used anywhere you can see, it could easily be used by some other code compiled separately, or accessed through reflection.
@@ -76,8 +76,8 @@
@ul
@li
- @b{Initial Compilation}: @code{.scala} files to @code{.class} and @code{.sjsir} files
- @li
+ @b{Initial Compilation}: @code{.scala} files to @code{.class} and @code{.sjsir} files
+ @li
@b{Optimization}: @code{.sjsir} files to smallish/fast @code{.js} files
@li
@b{Closure-Compiler}: @code{.js} files to smaller/faster @code{.js} files
@@ -100,7 +100,7 @@
@sect{Initial Compilation}
@p
As described earlier, the Scala.js compiler is implemented as a Scala compiler plugin, and lives in the main repository in @a("compiler/", href:="https://github.com/scala-js/scala-js/tree/master/compiler"). The bulk of the plugin runs after the @code{mixin} phase in the @a("Scala compilation pipeline", href:="http://stackoverflow.com/a/4528092/871202"). By this point:
-
+
@ul
@li
Types and implicits have all been inferred
@@ -112,14 +112,14 @@
@hl.scala{trait}s have been @a("replaced by interfaces and classes", href:="http://stackoverflow.com/a/2558317/871202")
@p
- Overall, by the time the Scala.js compiler plugin takes action, most of the high-level features of the Scala language have already been removed. Compared to a hypothetical, alternative "from scratch" implementation, this approach has several advantages:
+ Overall, by the time the Scala.js compiler plugin takes action, most of the high-level features of the Scala language have already been removed. Compared to a hypothetical, alternative "from scratch" implementation, this approach has several advantages:
@ul
@li
It helps ensure that the semantics of these features always, 100% match that of Scala-JVM
@li
It reduces the amount of implementation work required by re-using the existing compilation phases
-
+
@p
This first phase is mostly a translation from the Scala compiler's internal AST to the Scala.js Intermediate Representation, and does not contain very many interesting optimizations. At the end of the initial compilation, the Scala compiler with Scala.js plugin results in two sets of files:
@@ -169,7 +169,7 @@
@p
Notably, GCC @i{does not preserve the semantics of arbitrary Javascript}! In particular, it only works for a subset of Javascript that it understands and can properly analyze. This is an issue when hand-writing Javascript for GCC since it's very easy to step outside that subset and have GCC break your code, but is not a worry when using Scala.js: the Scala.js optimizer (the previous phase in the pipeline) automatically outputs Javascript which GCC understands and can work with.
@p
- GCC duplicates a lot of functionality that the Scala.js optimizer already does, such as DCE and inlining. It is entirely possible to skip the optimization phase, output the naive 20mb Javascript blob, and run GCC on it to bring the size down. However, GCC is much slower than the Scala.js optimizer, taking 60 seconds where the optimizer takes less than 1.
+ GCC duplicates a lot of functionality that the Scala.js optimizer already does, such as DCE and inlining. It is entirely possible to skip the optimization phase, output the naive 20mb Javascript blob, and run GCC on it to bring the size down. However, GCC is much slower than the Scala.js optimizer, taking 60 seconds where the optimizer takes less than 1.
@p
Empirically, running GCC on the output of the optimizer produces the smallest output blobs: ~150-400kb, significantly smaller than the output of running either of them alone, and so that is what we do. This takes 5-10 seconds to run, which makes it somewhat slow for iterative development, so it's typically only run right before final testing and deployment. This corresponds to the @code{fullOptJS} command in SBT.
diff --git a/book/src/main/scalatex/book/indepth/DesignSpace.scalatex b/book/src/main/scalatex/book/indepth/DesignSpace.scalatex
index abdd655..24e77ae 100644
--- a/book/src/main/scalatex/book/indepth/DesignSpace.scalatex
+++ b/book/src/main/scalatex/book/indepth/DesignSpace.scalatex
@@ -3,7 +3,7 @@
@sect("Why No Reflection?")
- @p
+ @p
Scala.js prohibits reflection as it makes dead-code elimination difficult, and the compiler relies heavily on dead-code elimination to generate reasonably-sized executables. The chapter on the Compilation Pipeline goes into more detail of why, but a rough estimate of the effect of various optimizations on a small application is:
@ul
@@ -11,13 +11,13 @@
@b{Full Output} - ~20mb
@li
@b{Naive Dead-Code-Elimnation} - ~800kb
- @li
+ @li
@b{Inlining Dead-Code-Elimnation} - ~600kb
@li
@b{Minified by Google Closure Compiler} - ~200kb
@p
- The default output size of 20mb makes the executables difficult to work with. Even though browsers can deal with 20mb Javascript blobs, it takes the browser several seconds to even load it, and up to a minute after that for the JIT to optimize the whole thing.
+ The default output size of 20mb makes the executables difficult to work with. Even though browsers can deal with 20mb Javascript blobs, it takes the browser several seconds to even load it, and up to a minute after that for the JIT to optimize the whole thing.
@sect{Dead Code Elimination}
@p
@@ -56,7 +56,7 @@
@sect{Whither Reflection?}
@p
To imagine why reflection makes this difficult, imagine a slightly modified program which includes some reflective calls in @hl.scala{App.main}
-
+
@hl.scala
@@JSExport
object App extends js.JSApp{
@@ -74,7 +74,7 @@
@p
Here, we're assuming @hl.scala{userInput()} is some method which returns a @hl.scala{String} that was input by the user or otherwise somehow decided at runtime.
@p
- We can start the same process: @hl.scala{App.main} is live since we @hl.scala{@@JSExport}ed it, but what objects or methods are reachable from @hl.scala{App.main}? The answer is: it depends on the values of @hl.scala{userInput()}, which we don't know. And hence we don't know which classes or methods are reachable! Depending on what @hl.scala{userInput()} returns, any or all methods and classes could be used by @hl.scala{App.main()}.
+ We can start the same process: @hl.scala{App.main} is live since we @hl.scala{@@JSExport}ed it, but what objects or methods are reachable from @hl.scala{App.main}? The answer is: it depends on the values of @hl.scala{userInput()}, which we don't know. And hence we don't know which classes or methods are reachable! Depending on what @hl.scala{userInput()} returns, any or all methods and classes could be used by @hl.scala{App.main()}.
@p
This leaves us a few options:
@@ -90,7 +90,7 @@
All three are possible options: Scala.js started off with #1. #3 is the approach used by @a("Proguard", href:="http://proguard.sourceforge.net/manual/examples.html#annotated"), which lets you annotate things e.g. @hl.scala{@@KeepApplication} to preserve things for reflection and preventing Proguard from eliminating them as dead code.
@p
- In the end, Scala.js chose #2. This is helped by the fact that overall, Scala code tends not to use reflection as heavily as Java, or dynamic languages which use it heavily. Scala uses techniques such as @a("lambdas", href:="http://docs.scala-lang.org/tutorials/tour/anonymous-function-syntax.html") or @a("implicits", href:="http://docs.scala-lang.org/tutorials/tour/implicit-parameters.html") to satisfy many use cases which Java has traditionally used reflection for, while friendly to the optimizer.
+ In the end, Scala.js chose #2. This is helped by the fact that overall, Scala code tends not to use reflection as heavily as Java, or dynamic languages which use it heavily. Scala uses techniques such as @a("lambdas", href:="http://docs.scala-lang.org/tutorials/tour/anonymous-function-syntax.html") or @a("implicits", href:="http://docs.scala-lang.org/tutorials/tour/implicit-parameters.html") to satisfy many use cases which Java has traditionally used reflection for, while friendly to the optimizer.
@p
There are a range of use-cases for reflection where you want to inspect an object's structure or methods, where lambdas or implicits don't help. People use reflection to @a("serialize objects", href:="http://jackson.codehaus.org/DataBindingDeepDive"), or for @a("routing messages to methods", href:="https://access.redhat.com/documentation/en-US/Fuse_ESB_Enterprise/7.1/html/Implementing_Enterprise_Integration_Patterns/files/BasicPrinciples-BeanIntegration.html"). However, both these cases can be satisfied by...
@@ -110,14 +110,14 @@
import upickle._
case class Thing(a: Int, b: String)
- write(Thing(1, "gg"))
+ write(Thing(1, "gg"))
// res23: String = {"a": 1, "b": "gg"}
@p
- Or to @a("route messages to the appropiate methods", href:="https://github.com/lihaoyi/autowire") without boilerplate, and @i{without} using reflection!
+ Or to @a("route messages to the appropiate methods", href:="https://github.com/lihaoyi/autowire") without boilerplate, and @i{without} using reflection!
@p
- The fact that you can satisfy these use cases with macros is non-obvious: in dynamic languages, macros only get an AST, which is basically opaque when you're only passing a single value to it. With Scala, you get the value @i{together with it's type}, which lets you inspect the type and generate the proper serialization/routing code that is impossible to do in a dynamic language with macros.
+ The fact that you can satisfy these use cases with macros is non-obvious: in dynamic languages, macros only get an AST, which is basically opaque when you're only passing a single value to it. With Scala, you get the value @i{together with it's type}, which lets you inspect the type and generate the proper serialization/routing code that is impossible to do in a dynamic language with macros.
@p
Using macros here also plays well with the Scala.js optimizer: the macros are fully expanded before the optimizer is run, so by the time the optimizer sees the code, there is no more magic left: it is then free to do dead-code-elimination/inlining/other-optimizations without worrying about reflection causing the code to do weird things at runtime. Thus, we've managed to substitute most of the main use-cases of reflection, and so can do without it.
@@ -140,12 +140,12 @@
@p
Scala.js works around this in the general case by adding a @hl.javascript{| 0} to the translation, e.g.
-
+
@hl.scala
/*JVM*/
15 / 4 // 3
@hl.javascript
- /*JS*/
+ /*JS*/
(15 / 4) | 0 // 3
@p
@@ -183,13 +183,13 @@
@p
The decision to not support these exceptional cases comes down to a value judgement: how often do people actually depend on an exception being thrown as part of their program semantics, e.g. by catching it and performing actions? And how often are they just a way of indicating bugs? It turns out that very few @hl.scala{ArithmeticException}s, @hl.scala{ArrayIndexOutOfBoundsException}s, or similar are actually a necessary part of the program! They exist during debugging, but after that, these code paths are never relied upon "in production".
@p
- Thus Scala.js goes for a compromise: in the Fast Optimization mode, we run the code with all these checks in place, so as to catch cases where these errors occur close-to-the-source and make it easy for you to debug them. In Full Optimization mode, on the other hand, we remove these checks, assuming you've already ran through these cases and found any bugs during development.
+ Thus Scala.js goes for a compromise: in the Fast Optimization mode, we run the code with all these checks in place, so as to catch cases where these errors occur close-to-the-source and make it easy for you to debug them. In Full Optimization mode, on the other hand, we remove these checks, assuming you've already ran through these cases and found any bugs during development.
@p
This is a common pattern in situations where there's a tradeoff between debuggability and speed. In Scala.js' case, it allows us to get good debuggability in development, as well as good performance in production. There's some loss in debuggability in development, sacrificed in exchange for greater performance.
-
-@sect("Why Jars instead of RequireJS/CommonJS")
- @p
+
+@sect("Why Jars instead of RequireJS/CommonJS")
+ @p
In JVM-land, the standard method for distributing these libraries is as @a("Maven Artifacts", href:="http://stackoverflow.com/questions/2487485/what-is-maven-artifact"). These are typically published in a public location such as @a("Maven Central", href:="http://search.maven.org/"), where others can find and download them for use. Typically downloads are done automatically by the build-tool: in Scala-JVM typically this is SBT.
@p
In Javascript-land, there are multiple ways of acquiring dependencies: @a("CommonJS", href:="http://en.wikipedia.org/wiki/CommonJS") and @a("RequireJS/AMD", href:="http://requirejs.org/") are two competing standards with a host of implementations. Historically, a third approach has been most common: the developer would simply download the modules himself, check it into source-control and manually add a @hl.html{<script>} tag to the HTML page that will make the functionality available through some global variable.
diff --git a/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex b/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex
index 89f0a57..fe5c2df 100644
--- a/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex
+++ b/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex
@@ -4,18 +4,18 @@
There are many reasons you may want to port a Java class to Scala.js: you want to use it directly, you may be trying to port a library which uses it. In general, we haven't been porting things "for fun", and obscure classes like @hl.scala{org.omg.corba} will likely never be ported: we've been porting things as the need arises in order to support libraries (e.g. @a("Scala.Rx", href:="https://github.com/lihaoyi/scala.rx") that need them.
@sect{Available Java APIs}
-
+
@ul
- @for(data <- Book.javaAPIs)
+ @for(data <- BookData.javaAPIs)
@li
@a(data._1, href:=data._2)
-
+
@sect{Porting Java APIs}
@p
The process for making Java library classes available in Scala.js is relatively straightforward:
@ul
@li
- Find a class that you want to use in Scala.js, but is not implemented.
+ Find a class that you want to use in Scala.js, but is not implemented.
@li
Write a clean-room implementation in Scala, without looking at the source code of @a("OpenJDK", href:="http://openjdk.java.net/"). This is due to legal-software-license incompatibility between OpenJDK and Scala.js. Reading the docs or specification are fine, as is looking at the source of alternate implementations such as @a("Harmony", href:="http://harmony.apache.org/")
@li
diff --git a/book/src/main/scalatex/book/indepth/SemanticDifferences.scalatex b/book/src/main/scalatex/book/indepth/SemanticDifferences.scalatex
index 3c89720..bbc86db 100644
--- a/book/src/main/scalatex/book/indepth/SemanticDifferences.scalatex
+++ b/book/src/main/scalatex/book/indepth/SemanticDifferences.scalatex
@@ -36,7 +36,7 @@
@hl.scala{2147483648} (> @hl.scala{Int.MaxValue}) matches @hl.scala{Float}, @hl.scala{Double}
@li
@hl.scala{1.2} matches @hl.scala{Float}, @hl.scala{Double}
- @p
+ @p
As a consequence, the following apparent subtyping relationship holds:
@hl.scala
@@ -111,7 +111,7 @@
Note that this also includes calls like
@hl.scala
val A,B,C,D = Value
- @p
+ @p
since they are desugared into separate val definitions.
@p
Calls to either of these two methods which could not be rewritten, or calls to constructors of the protected Val class without an explicit name as parameter, will issue a warning.
@@ -133,11 +133,11 @@
@p
This table gives a quick overview of the sorts of libraries you can and can't use when working on Scala.js:
- @table
+ @table
@thead
@th{Can Use}@th{Can't Use}
@tbody
- @for(tuple <- Book.myTable)
+ @for(tuple <- BookData.myTable)
@tr
@td{@tuple._1}@td{@tuple._2}
@@ -145,11 +145,11 @@
We'll go into each section bit by bit
@sect{Standard Library}
- @table
+ @table
@thead
@th{Can Use}@th{Can't Use}
@tbody
- @for(tuple <- Book.myTable.slice(0, 3))
+ @for(tuple <- BookData.myTable.slice(0, 3))
@tr
@td{@tuple._1}@td{@tuple._2}
@@ -159,11 +159,11 @@
There isn't a full list of library APIs which are available from Scala.js, but a rough idea can be had from looking the the source code @a("on", href:="https://github.com/scala-js/scala-js/tree/master/javalanglib/src/main/scala/java/lang") @a("github", href:="https://github.com/scala-js/scala-js/tree/master/javalib/src/main/scala/java").
@sect{Reflection v.s. Macros}
- @table
+ @table
@thead
@th{Can Use}@th{Can't Use}
@tbody
- @for(tuple <- Book.myTable.slice(3, 4))
+ @for(tuple <- BookData.myTable.slice(3, 4))
@tr
@td{@tuple._1}@td{@tuple._2}
@@ -171,31 +171,31 @@
As described @a("here"), in Reflection is not supported in Scala.js, due to the way it inhibits optimization. This doesn't just mean you can't use reflection yourself: many third-party libraries also use reflection, and you won't be able to use them either.
@p
- On the other hand, Scala.js does support Macros, and macros can in many ways substitute many of the use cases that people have traditionally used reflection for. For example, instead of using a reflection-based serialization library like @code{scala-pickling}, you can use a macro-based library such as @code{upickle}.
+ On the other hand, Scala.js does support Macros, and macros can in many ways substitute many of the use cases that people have traditionally used reflection for. For example, instead of using a reflection-based serialization library like @code{scala-pickling}, you can use a macro-based library such as @code{upickle}.
@sect{Pure-Scala v.s. Java Libraries}
- @table
+ @table
@thead
@th{Can Use}@th{Can't Use}
@tbody
- @for(tuple <- Book.myTable.slice(4, 5))
+ @for(tuple <- BookData.myTable.slice(4, 5))
@tr
@td{@tuple._1}@td{@tuple._2}
@p
Scala.js has access to any pure-Scala libraries that you have cross-compiled to Scala.js, and cross-compiling a pure-Scala library with no dependencies is straightforward. Many of them, such as the ones listed above, have already been cross-compiled and can be used via their maven coordinates.
@p
- You cannot use any libraries which have a Java dependency. This means libraries like Scalatest or Scalate, which depend on a number of external Java libraries or source files, cannot be used from Scala.js. You can only use libraries which have no dependency on Java libraries or sources.
+ You cannot use any libraries which have a Java dependency. This means libraries like Scalatest or Scalate, which depend on a number of external Java libraries or source files, cannot be used from Scala.js. You can only use libraries which have no dependency on Java libraries or sources.
@sect{Javascript APIs v.s. JVM APIs}
- @table
+ @table
@thead
@th{Can Use}@th{Can't Use}
@tbody
- @for(tuple <- Book.myTable.slice(5, 7))
+ @for(tuple <- BookData.myTable.slice(5, 7))
@tr
@td{@tuple._1}@td{@tuple._2}
- @p
+ @p
Apart from depending on Java sources, the other thing that you can't use in Scala.js are JVM-specific APIs. This means that anything which goes down to the underlying operating system, filesystem, GUI or network are unavailable in Scala.js. This makes sense when you consider that these capabilities are no provided by the browser which Scala.js runs in, and it's impossible to re-implement them ourselves.
@p
In exchange for this, Scala.js provides you access to Browser APIs that do related things. Although you can't set up a HTTP server to take in-bound requests, you can make out-bound requests using @hl.scala{dom.XMLHttpRequest} to other servers. You can't write to the filesystem or databases directly, but you can write to the @hl.scala{dom.localStorage} provided by the browser. You can't use Swing or AWT or WebGL but instead work with the DOM and Canvas and WebGL.
@@ -204,18 +204,18 @@
@sect{Java tooling v.s. Scala/Browser tooling}
- @table
+ @table
@thead
@th{Can Use}@th{Can't Use}
@tbody
- @for(tuple <- Book.myTable.slice(7, 8))
+ @for(tuple <- BookData.myTable.slice(7, 8))
@tr
@td{@tuple._1}@td{@tuple._2}
-
+
@p
Lastly, there is the matter of tools. Naturally, all the Scala tools which depend on the JVM are out. This means things like the Yourkit, VisualVM and JProfiler profilers, as well as things like the Scala command-line REPL which relies on classloaders and other such things to run on the JVM
@p
On the other hand, you do get to keep and continue using many tools which are build for Scala but JVM-agnostic. For example, IDEs such a IntelliJ and Eclipse work great with Scala.js; from their point of view, it's just Scala, and things like code-navigation, refactoring and error-highlighting all work out of the box. SBT works with Scala.js too, and you see the same compile-erorrs in the command-line as you would in vanilla Scala, and even things like incremental compilation work un-changed.
@p
- Lastly, you gain access to browser tools that don't work with normal Scala: you can use the Chrome or Firefox consoles to poke at your Scala.js application from the command line, or their profilers/debuggers. With source maps set up, you can even step-through debug your Scala.js application directly in Chrome.
+ Lastly, you gain access to browser tools that don't work with normal Scala: you can use the Chrome or Firefox consoles to poke at your Scala.js application from the command line, or their profilers/debuggers. With source maps set up, you can even step-through debug your Scala.js application directly in Chrome.