DEV Community

Cover image for Trying out Transcrypt
Dustin King
Dustin King

Posted on

Trying out Transcrypt

One thing that doesn't make sense to me is that you can't just use any language anywhere. In particular, why can't you use Python for client-side web development?

I've been told this is unrealistic for a couple reasons. Performance is cited, as any other language would have to be either compiled to JavaScript, or a large language runtime, hopefully in asm.js or WebAssembly, would have to be downloaded before the actual scripts could be run. The other issue is debuggability: in some other language, you wouldn't be able to inspect and step into your source files in the browser's dev tools. Transcrypt attempts to address both of these concerns.

What is Transcrypt?

Transcrypt is a Python compiler, written in Python, that outputs JavaScript. The standard Python implementation, CPython, has some facilities for inspecting the abstract syntax tree of a program, and Transcrypt makes use of these, as well as Google's Closure compiler for generating JavaScript code. Its output is a JavaScript file with exported functions and variables like any other JavaScript file, and with the ability to access external JavaScript variables and functions as well.

Transcrypt aims to be a "90% complete" implementation of Python that favors speed over purity. In my test project, I haven't run into any of the things that are "incomplete". I did run into one small performance issue that was easily fixed. As far as debuggability goes, Transcrypt generates "sourcemaps", which allow you to debug using the browser's devtools.

My Test Project

To try out Transcrypt, I ported a JavaScript program I had written a couple years ago to Python. The program uses the canvas element to draw a bunch of particles, and simulates their movement as they all pull on each other with gravity. Since every particle pulls on every other particle, according to their distance and masses, it's an N^2 algorithm. Not very efficient, but for testing the performance of the compiled code that's not a bad thing.

# force of gravitation between pararticles p1 and p2,
# given their masses p1.m, p2.m and gravitational constant g
def grav(p1, p2, g):
    r = dist(p1, p2)
    if (r == 0):
        r = 0.00000001
    return g * p1.m * p2.m / sq(r)

# apply force of gravitation to particle p1's velocity from p2,
#  given grav constant g
def nudge(p1, p2, g):
    f = grav(p1, p2, g)
    theta = Math.atan2( (p2.y - p1.y), (p2.x - p1.x) )
    gravX = f * Math.cos(theta)
    gravY = f * Math.sin(theta)
    p1.vX += gravX / p1.m
    p1.vY += gravY / p1.m

    p1.vX, p1.vY = trimSpeed(p1.vX, p1.vY, MAX_SPEED)

# Let each particle pull on each other particle
def gravParticles(g):
    for p1 in particles:
        for p2 in particles:
            if p1 is not p2:
                nudge(p1, p2, g)
Enter fullscreen mode Exit fullscreen mode

Small equally sized red squares scattered on a white background.  The scattering is mostly random but there are a couple clusters and a few sparse places. You can't tell, but these particles are all gravitating toward each other.

After some yak shaving to get the right version of CPython installed (the Transcrypt website says 3.6 is required), "pip3 install transcrypt" worked without a hitch.

My approach was to start with an empty python file, move the functions over from the existing JavaScript file one at a time (fixing indentation, braces, etc. as necessary), and after each one recompile it and make sure the program still runs. This worked fine until, at some point, things got choppy.

Performance

After moving various pieces back and forth between the Python and JavaScript files, I determined that the problem was this line:

return { 'vX': vXNew, 'vY': vYNew }
Enter fullscreen mode Exit fullscreen mode

Since it's part of my "trimSpeed" function that's called for every pair of particles (of which there are 200), and the expected time between cycles is 50 milliseconds (or 20 fps), that means this code should be called nearly 200 * 200 * 20 = 800,000 times a second. The reason for the slowness is probably that Transcrypt is creating not just a plain JavaScript object, but a Python "dict".

Since all I'm trying to do here is return the X and Y components of the speed, I could switch to the more Pythonic multiple return statement:

return vXNew, vYNew
Enter fullscreen mode Exit fullscreen mode

This removed the choppiness.

The Main Loop

The last things to port were the outer function that I call from the body tag's onload attribute, and an inner function that I pass to window.requestAnimationFrame(). The inner function is basically the "game loop" of the program (minus the user input and AI steps). requestAnimationFrame also hints to the browser that some GPU acceleration would be nice (and it is nice).

This could be refactored quite a bit (not to mention my inconsistent abuse of global variables), but I decided to port what was left of it wholesale:

def doit():
    canvas = document.getElementById('tutorial')
    CANVAS_WIDTH = canvas.width
    CANVAS_HEIGHT = canvas.height
    ctx = canvas.getContext('2d')

    init(particles, CANVAS_WIDTH, CANVAS_HEIGHT)

    last = None
    def go(timestamp):
        global last
        if not last:
            last = timestamp
        if timestamp - last >= 50:
            g = GRAVITATIONAL_CONSTANT
            gravParticles(g)
            moveParticles(CANVAS_WIDTH, CANVAS_HEIGHT)
            drawParticles(ctx, CANVAS_WIDTH, CANVAS_HEIGHT)
            last = timestamp

        # make this function keep looping
        window.requestAnimationFrame(go)

    # kick things off initially
    window.requestAnimationFrame(go)
Enter fullscreen mode Exit fullscreen mode
...

<!-- the compiled python code -->
<script src="__javascript__/ent_tr.js"></script>

<!-- the original script (no longer used) -->
<!-- <script src="ent_tr_old.js"></script> -->
...
<body onload="ent_tr.doit();">
    <!-- yes, I evolved this from a 'hello world' canvas tutorial. don't judge me. -->
    <canvas id="tutorial" width="1000" height="500"></canvas>
</body>
Enter fullscreen mode Exit fullscreen mode

Other than differences of braces, parentheses, etc. this could pretty much be JavaScript code. As you can see, JavaScript APIs like document, window, and canvas are available from within Python code when compiled with Transcrypt.

File Size

How big are the files it generates? For a 4 KB Python source file (ent_tr.py), the following files were generated:

  • ent_tr.js (77 KB)
  • ent_tr.mod.js (5 KB)
  • ent_tr.js.map (129 KB)
  • ent_tr.mod.js.map (5 KB)

I believe ent_tr.js contains the Transcrypt runtime, so there should only need to be one in a given application. ent_tr.mod.js, corresponding to the module represented by the ent_tr.py file itself, is not much bigger than its source Python file. The *.map files are the sourcemaps, so they shouldn't need to be downloaded unless you're debugging.

Takeaways

I think this shows that Transcrypt is fast enough. One thing it appears to lack is the ability to include HTML, CSS, and Python in the same file, as one would with JSX and React. So Transcrypt might not currently be the best language for building user interfaces. One use case would be if you have (or plan to create) a moderately sized Python library that you would like to make available to other client-side code. If you don't want to maintain a separate JavaScript codebase with the same functionality, but want it to be more responsive than if you turned it into a microservice, Transcrypt may be for you.

I hope you found this interesting. I'll hopefully write more on Transcrypt as I continue to experiment with it. What do you think? Is 80 KB too big for a runtime? What benchmarks should we use to determine if it's "fast enough"?

A brilliant blue snake hangs from a tree (out of frame) in front of an out of focus, leafy background. Its large black eye stares at the camera. Does it have a gift for you?

Top comments (0)