[Tip] Running JavaScript in Shortcuts (iOS, macOS)

Changelog


Intro

For data processing that involves a large number of operations and/or loops, JavaScript can be much faster than built-in shortcut actions. Running JS using the data URI scheme (https://​en.wikipedia.org/​wiki/​Data_​URI_​scheme) has been a long-known trick, so I’ll try to summarize the correct way to do it.

The trick works because Shortcuts runs embedded JS code when it tries to render an HTML page into Rich Text or Web Archive (https://​en.wikipedia.org/​wiki/​Web_​Archive_​(file_​format)). Callbacks or async functions are not supported (unless they finish within 0.2 sec). For networking, you can use XMLHttpRequest (https://​developer.mozilla.org/​en-US/​docs/​Web/​API/​XMLHttpRequest_​API/​Using_​XMLHttpRequest).

Data URI

Provide your JS code inside the <script> tags along with the data:text/html prefix in a URL action:

data:text/html;charset=utf-8,<body/><script> YOUR CODE HERE </script>

js01

The charset=utf-8 part is necessary to handle Unicode characters. If some html or css is also needed, you can use <meta charset="UTF-8"> instead.

No other tags such as <html> or <header> are required, and the code part doesn’t have to be base64-encoded. You can enter code in a Text action and pass it to the URL action because it’s easier to type spaces and returns.

Input

Inserting variables directly into JS code is possible, but only if you know it’s safe to do so:

js02

This can fail if the text contains characters such as " or \ or strings like </script>, or if the number is Eastern Arabic like ١٢٣ or contains a decimal comma like 3,14 etc etc. Therefore, it is best to pass input variables in a dictionary using the “Set Dictionary Value” or “Dictionary” action:

js03

Starting from iOS 17, JS code fails to run if it contains the # or :hash: symbol. In JS code, fortunately, these symbols most likely appear within string literals; in this case, you can replace \u0023 with \\u0023:

js04

If they appear elsewhere such as in html or css, the entire data needs to be base64-encoded. This is much slower than Replace Text. Please note that a base64 data URI split into every 76 characters runs faster than the same base64 URI in a single line.

js05

Output

Output should be written onto the page body document.body.textContent. To prevent output alterations (e.g. changing &lt; to <, a new line or multiple white spaces into a single space, losing <tag-like> strings), it is safer to URL-encode it using encodeURIComponent(). And again, to pass values correctly for various regions, it is better to produce the output as a JSON object and ‘stringify’ it using JSON.stringify()

js06

Note: <script>document.write(output)</script> can be used instead of <body/><script>document.body.textContent = output</script> but it consumes more memory.

HTML Rendering

Since Shortcuts treats web pages as Rich Text, the final output from a rendered page can be converted to text easily. After that, the URL-encoded output has to be decoded back. There are four known ways to produce URL-decoded text output from a data URI.

  • Get Contents of Web Page

Note: This method asks to allow web content access.

js07

  • Get File of Type public.rtf

js08

  • Get File of Type com.apple.webarchive

js09

  • URL as Rich Text

js10

They generate the same result at about the same speed, but the last one consumes less memory because it has one less action.

Maximum memory usage (up to about 200MB depending on device) and maximum output file size (up to about 50MB depending on device) are all the same. Execution halts if exceeded.

Finally, here’s a sample shortcut that combines all of the above:

https://​www.icloud.com/​shortcuts/​bd8dcf00d3cd418a8e90789df24dc0bd

js11


Run JavaScript on Web Page

This native action runs JavaScript on an already-loaded Safari web page. You can run async code here. Starting from iOS 18.4, white spaces cause unexpectedly longer execution time. This happened before as well, but it has become way more dramatic.

For example, the default code that comes with the action takes 5.1 seconds on a specific web page:

var result = [];
// Get all links from the page
var elements = document.querySelectorAll("a");
for (let element of elements) {
    result.push({
        "url": element.href,
        "text": element.innerText
    });
}

// Call completion to finish
completion(result);

It becomes 3.2 sec by removing comments that contain some spaces:

var result = [];
var elements = document.querySelectorAll("a");
for (let element of elements) {
    result.push({
        "url": element.href,
        "text": element.innerText
    });
}

completion(result);

If there are less than about 20 white spaces, then the time is close to the minimum, at 1.3 sec:

var result = [];
var elements = document.querySelectorAll("a");
for (let element of elements) {
result.push({
"url": element.href,
"text": element.innerText
});
}

completion(result);

Removing more spaces doesn’t get faster than 1.3 sec:

var result=[];var elements=document.querySelectorAll("a");for(let element of elements){result.push({"url":element.href,"text":element.innerText});}completion(result);

However, removing spaces this way is not practical with bigger code. Instead, you can URL encode the whole code and put it inside the following command:

completion(new Function(decodeURIComponent("URL Encoded Text"))());

Note that completion(result) has to be changed to return result.

(Credit to FedIz on RoutineHub Discord)

js12

9 Likes

Updated the bug section and the sample shortcut because the data URI bug occurs with the :hash: emoji as well as the # symbol. The proper workaround is to replace \u0023 with \\u0023 with regex on.

2 Likes

Hey @gluebyte!

Do you know if there is anyway to use JavaScript to capture data from a button, and close a web view?

I created a Shortcut that uses Simple.css to clean up web views and I’d like to see if I could add some more features.

Simple Web View on RoutineHub

Thanks!

Thank you for all the work you put into this and many other guides that helped me learn how to use Shortcuts throughout the year. I keep revisiting this with the same question: under what circumstances does executing JavaScript in Shortcuts (by this or any other means) actually outperform a native implementation? Both in real world and synthetic testing, across small and obscenely large corner cases, I have found native iOS actions to outperform JavaScript in equivalent logic, and to even match JS on memory-consuming kludges (e.g., reversing a list). I think this is because of COM interfaces and private APIs and compiled code yada yada, and that there are thus threading/execution bottlenecks that JavaScriptCore simply cannot optimize away, while for the rest, it’s unlikely that a JavaScript engine (even V8) would ever optimize to parity with compiled code. I imagine you have found cases where JavaScript outperforms native, but this is unexpected to me. What say you?

In small use cases, native actions are faster because the JS method requires full page rendering. However, for tasks that need nested repeat loops or a lot of slow actions such as split/match/combine text, etc., JS is way faster.

In short, JS takes less than a few seconds at most, while native actions can take minutes in some cases. Some time ago I converted the core part of my Dictionary Action Builder shortcut from native actions to JS and the difference was more than 100x. You can try v1.1 and v1.2 for yourself: https://routinehub.co/shortcut/4626/changelog

1 Like

Its not working for me.

Here is minimal example:

Try tapping the URL variable in the URL Decode action and changing the type to Rich Text

2 Likes

Yes it worked, thank you.

Ah now I get it. Very helpful illustrative example, thank you. Even my gigantic tasks are not that gigantic it turns out, with the long pole tending to be app actions. Now you’ve got me thinking about potentially offloading more logic to scripting, e.g. parsing really large documents using JavaScript. That’s the sort of stuff I’d manually chunk and sometimes deal with timeouts, maybe using JS will allow me to trade some of that timeout for memory etc.

Article updated with more and up-to-date info

2 Likes