Changelog
- 2025-10-27
- Restructured content with more details
- Added section on Run JavaScript on Web Page
- 2023-09-27
- Moved to Automators Talk
- Corrected speed comparison of rendering methods
- Added info on iOS 17 bug
- 2022-09-30
- Initially posted on Reddit (https://www.reddit.com/r/shortcuts/comments/xs5xtm/)
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>

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:

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:

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

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.

Output
Output should be written onto the page body document.body.textContent. To prevent output alterations (e.g. changing < 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()

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.

- Get File of Type
public.rtf

- Get File of Type
com.apple.webarchive

- URL as Rich Text

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

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)

