History of JS interop in Dart
Support for Wasm just landed in the current Flutter beta, thanks to an exciting JavaScript interop milestone reached in Dart 3.3. To…
Support for Wasm just landed in the current Flutter beta, thanks to an exciting JavaScript interop milestone reached in Dart 3.3. To celebrate, we’re taking a look back at the decade-long journey of Dart and JS interoperability.
#
Interoperability has been a core focus from the beginning of Dart. When Dart was first released in 2011, it was designed to be embeddable and multi-platform. It ran on a standalone virtual machine, embedded in a browser, and compiled to JavaScript. When Flutter came along in 2015, we were ready to embed it there, too. Now, we’re excited to target WasmGC runtimes, as well.
At first, we worked quickly to expose the capabilities of
each platform where Dart was embedded. That’s how our SDK
platform-specific libraries emerged:
dart:io
exposed the file system on the VM,
dart:html
exposed the browser APIs on the web, and so on. These
libraries looked and felt like regular Dart libraries, but
behind the scenes hid some sophisticated low-level, native
primitives to make them work. This was the very first form
of interop we ever invented. It was expressive, but
restricted to only SDK libraries.
On the web, developers needed access to more than just
browser APIs. So we started looking at ways to open
interoperability to cover more targets. As a starting point,
we introduced
dart:js
in 2013 to enable access to JavaScript libraries.
// Short example JavaScript code to illustrate Dart/JS interop
window.myTopLevel = {
field1: 0,
method2() {
return this.field1;
}
}
// Access via `dart:js` (2013)
import 'dart:js' as js;
void main() {
// This line has a typo! oops :(
var object = js.context['myTopLevl'];
object['field1'] = 1;
// This call fails with a noSuchMethod because method2
// returns an int, oops
object.callMethod('method2', []).substr(1);
}
We knew then that dart:js was not the
programming model we wanted. You had to use strings to
access names from JavaScript — forget about finding issues
at compile-time, and don’t even think about code completion!
The implementation was expensive, too. It heavily relied on
boxes and deep copies for most operations. So we continued
drafting
ideas in 2014 and 2015 until v0.6 of
package:js was released.
// Access via `package:js` (2015)
import 'package:js/js.dart';
// Magic annotations allow us to declare API signatures:
@JS()
class MyObject {
external int get field1;
external void set field1(int value);
external String method2();
}
@JS()
external MyObject get myTopLevel;
void main() {
// Access to code is less error prone: analyzer can check that
// these symbols match a declaration, and we get code-completion too!
var object = myTopLevel;
object.field1 = 1;
// But types are not checked, this unsoundly invokes substring on an int
object.method2().substring(1);
}
With
package:js
we finally had an open API that was efficient and user
friendly. You could sprinkle some annotations on abstract
classes, and voila, you had access to JavaScript
APIs. It all worked like magic, until it didn’t. There was a
lot you couldn’t do with package:js: accessing
browser APIs directly, renaming members, conversions,
attaching Dart logic, and
more. To compensate, we also shipped
dart:js_util
— a lightweight and efficient low-level API similar to
dart:js, as a fallback. All the limitations in
package:js really bothered us, but our hands
were tied. We needed more from the Dart language to do
better.
Around that time, we were already working on the biggest
change to the language we have ever made — we were making
Dart
sound. Ironically, when we released the new type system with
Dart 2.0 in 2018, interoperability got worse!
Beyond those early limitations, that magic that made
package:js special had a dark side — it
couldn’t check the validity of types. This meant that our
interoperability was a source of unsoundness in our
otherwise sound language.
Then, our journey changed to focus on improving both Dart and JS-interop as a concerted effort. With clear principles (be idiomatic, expressive, compositional, precise, approachable, pragmatic, non-magical, and complete) we steered towards a design that anchored on typing and static dispatching, and that challenged the Dart language. What followed was a side-by-side evolution.
-
In 2019, Dart 2.7 added static extension methods. You could attach custom Dart logic to a JS-interop class and convert values, like a JS
Promiseinto a DartFuture, without using wrappers. -
In 2021, we released
@staticInteropwithpackage:jsv0.6.4. At last, JS-interop was expressive enough — you could expose browser APIs that previously were exclusively managed by SDK libraries likedart:html. -
In 2023, when we dropped unsound null safety in Dart 3.0, we could finally see the progress we had made, our designs and
@staticInteropwork made it clear we were ready to address the soundness gap we had for so long.
That year, we introduced compilation to WasmGC and leveraged JS-interop to run rich frameworks like Flutter web on it. This sparked work on JS Types to clearly define the Dart and JS boundary in the programming model and find a consistent way to work with JS in both Wasm and JS compilation targets. We also started the extension types language experiment — a feature launched in Dart 3.3 that bridges the gap between the Dart language and JS-interop. For years, JS-interop had behaviors, like type erasure, that didn’t match anything else in Dart. With extension types, JS-interop could finally be idiomatic and get the support it deserves in Dart development tools.
Despite the many shifts and turns along the way, one thing
remained consistent throughout the entire decade: the active
engagement of our Dart community. Community members took
early steps testing and contributing to
dart:js, then later influencing the design of
package:js. They wrote tools to address feature
gaps (package:js_wrapping), and experimented with ways to improve productivity by
autogenerating Dart APIs (package:js_facade_gen,
package:js_bindings,
package:typings). Each contribution helped make Dart’s interop design
better. To each of you out there, thank you for making this
such an exciting adventure!
Finally, here we are in 2024. We released
dart:js_interop
in Dart 3.3 together with
package:web, the newest solutions for JS interop in Dart that make
compiling
Flutter to Wasm
possible.
// Access via `dart:js_interop` (2024)
import 'dart:js_interop';
// Declarations use extension types, which are very similar to package:js
// declarations. The main difference: they are statically dispatched.
extension type MyObject._(JSObject _) implements JSObject {
external int get field1;
external void set field1(int value);
external String method2();
}
@JS()
external MyObject get myTopLevel;
void main() {
var object = myTopLevel;
object.field1 = 1;
// At last, access is sound - this line fails with a type error
// when returning from method2.
object.method2().substring(1);
}
-
dart:js_interopis a static, sound, idiomatic, expressive, and consistent form of interop based on extension types that is capable of exposing any JavaScript or browser APIs. -
package:webusesdart:js_interopto do whatdart:htmlonce did 13 years ago, but in a way that is supported both in JavaScript and WasmGC.
Today, we are excited to celebrate a new form of Dart/JS interop and the future it enables. Knowing our past, we are certain this isn’t the end of the journey, but an exciting point in our history.
We can’t wait to see what you’ll build with it!
More from Dart
Dart DevTools: Analyzing application performance with the CPU Profiler
Whether you’re a backend developer writing a command line tool using Dart, or a UX engineer building applications with Flutter, program performance is critical to the success of your project. Command line tools should minimize latency and apps should be responsive and snappy with no dropped frames. As developers, we try our best to write performant code but sometimes it’s not clear why our code isn’t performing as well as we would expect.
Better isolate management with Isolate.run()
Dart 2.19 introduces a new function that makes implementing concurrency as simple as a single line of code.