I was curious to see if they did something special when they detect a page is using AMP
(spoiler alert: they do not),
so I quickly hacked together a fake AMP page that seemingly fulfilled their simple test.
<html⚡️> <body>Fake AMP</body> </html>
I am a big emoji fan, so instead of the
<html amp>
variant, I went for the <html ⚡> variant and entered the ⚡ via the macOS emoji picker.
To my surprise, Facebook logged "FBNavAmpDetect: false". Huh 🤷♂️?
My first reaction was: <html ⚡️> does not quite look like what the founders of HTML had in mind,
so maybe hasAttribute()
is specified to return false
when an attribute name is invalid.
But what even is a valid attribute name?
I consulted the HTML spec
where it says (emphasis mine):
Attribute names must consist of one or more characters
other than controls, U+0020 SPACE, U+0022 ("), U+0027 ('), U+003E (>), U+002F (/), U+003D (=),
and noncharacters. In the HTML syntax, attribute names, even those for foreign elements,
may be written with any mix of ASCII lower and ASCII upper alphas.
I was on company chat with Jake Archibald at that moment,
so I confirmed my reading of the spec that ⚡ is not a valid attribute name.
Turns out, it is a valid name, but the spec is formulated in an ambiguous way, so Jake filed
"HTML syntax" attribute names.
And my lead to a rational explanation was gone.
Luckily a valid AMP boilerplate example
was just a quick Web search away, so I copy-pasted the code and Facebook, as expected,
reported "FBNavAmpDetect: true".
I reduced the AMP boilerplate example until it looked like my fake AMP page,
but still Facebook detected the modified boilerplate as AMP, but did not detect mine as AMP.
Essentially my experiment looked like the below code sample.
Perfect Heisenbug?
An invisible code point which specifies that the preceding character should be displayed
with emoji presentation. Only required if the preceding character defaults to text presentation.
You may have seen this in effect with the Unicode snowman that appears in a textual ☃︎
as well as in an emoji representation ☃️ (depending on the device you read this on,
they may both look the same).
As far as I can tell, Chrome DevTools prefers to always render the textual variant,
as you can see in the screenshot above.
But with the help of the
length()
and the
charCodeAt()
functions, the difference gets visible.
The macOS emoji picker creates the variant ⚡️, which includes the Variation Selector-16,
but AMP requires the variant without, which I have also confirmed in the
validator code.
You can see in the screenshot below how the AMP Validator
rejects one of the two High Voltage symbols.
I have filed crbug.com/1033453 against the Chrome DevTools
asking for rendering the characters differently, depending on whether the Variation Selector-16
is present or not.
Further, I have opened a feature request on the AMP Project repo demanding that
AMP should respect ⚡️ apart from ⚡.
Same same, but different.
Both
Facebook's Android app
as well as
Facebook's iOS app use a so-called in-app browser,
sometimes also referred to as IAB.
The core argument for using an in-app browser (instead of the user's default browser)
is to keep users in the app and to enable closer app integration patterns
(like "quote from a page in a new Facebook post"), while making others harder or even impossible
(like "share this link to Twitter").
Technically, IABs are implemented as
WebViews on Android,
respectively as
WKWebViews on iOS.
To simplify things, from hereon, I will refer to both simply as WebViews.
In-App Browsers are less capable than real browsers ⚓
Turns out, WebViews are rather limited compared to real browsers like Firefox, Edge, Chrome,
and to some extent also Safari.
In the past, I have done some research
on their limitations when it comes to features that are important in the context of Progressive Web Apps.
The linked paper has all the details, but you can simply see for yourself by opening
the 🕵️ PWA Feature Detector app
that I have developed for this research in your regular browser,
and then in a WebView like Facebook's in-app browser (you can share the
link visible to just yourself on Facebook
and then click through, or try to open my
post in the app).
On top of limited features, WebViews can also be used for effectively conducting intended
man-in-the-middle attacks,
since the IAB developer can arbitrarily
inject JavaScript code
and also
intercept network traffic.
Most of the time, this feature is used for good.
For example, an airline company might reuse the 💺 airplane seat selector logic
on both their native app as well as on their Web app.
In their native app, they would remove things like the header and the footer,
which they then would show on the Web (this is likely the origin of the
CSS can kill you meme).
For these reasons, people like Alex Russell—whom
you should definitely follow—have been advocating against WebView-based IABs.
Instead, you should wherever possible use
Chrome Custom Tabs on Android,
or the iOS counterpart
SFSafariViewController.
Alex writes:
Note that responsible native apps have a way of creating an "in app browser" that doesn't subvert user choice or break the web:
The other day, I learned with great joy that Facebook finally have marked their IAB debuggable 🎉.
Patrick Meenan—whom you should likewise follow
and whom you might know from the amazing WebPageTest
project—writes in a Twitter thread:
You can now remote-debug sites in the Facebook in-app browser on Android.
It is enabled automatically so once your device is in dev mode with USB debugging
and a browser open just visit chrome://inspect to attach to the WebView.
The browser (on iOS and Android) is just a WebView so it should behave
mostly like Chrome and Safari but it adds some identifiers to the UA string
which sometimes causes pages that UA sniff to break.
Finally, if your analytics aren't breaking out the in-app browsers for you,
I highly recommend you see if it is possible to enable.
You might be shocked at how much of your traffic comes from an in-app browser
(odds are it is the 3rd most common browser behind Chrome and Safari).
I have thus followed up on the invitation and had a closer look at their IAB by inspecting
example.org and also a simple test page
facebook-debug.glitch.me that contains the
debugger
statement in its head.
I have linked a debug trace 📄 that you can open for yourself
in the Performance tab of the Chrome DevTools.
As pre-announced by Patrick, Facebook's IAB changes the user-agent string.
The
default WebView user-agent string
looks something like Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.65 Mobile Safari/537.36
Facebook's IAB browser currently sends this:
navigator.userAgent // "Mozilla/5.0 (Linux; Android 10; Pixel 3a Build/QQ2A.191125.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/250.0.0.14.241;]"
Compared to the default user-agent string, the identifying bit is the suffix [FB_IAB/FB4A;FBAV/250.0.0.14.241;].
Facebook runs some performance monitoring via the
Performance interface.
This is split up in two scripts, each of which they seem to run three times.
They also check if a given page is using AMP
by checking for the presence of the amp or ⚡️ attribute on <html>.
They run some Feature Policy
tests via a function named getFeaturePolicyAllowListOnPage().
You can see the documentation for the tested
directives
on the Mozilla Developer Network.
Not all directives are currently supported by the WebView, so a number of warnings are logged.
The recognized ones (i.e., the output of the getFeaturePolicyAllowListOnPage() function above)
result in an object as follows.
I checked the response and request headers, but nothing special stood out.
The only remarkable thing given that they look at Feature Policy is the absence of the
Feature-Policy header.
All in all, these are all the things Facebook did that I could observe
on the pages that I have tested.
I didn't notice any click listeners or scroll listeners
(that could be used for engagement tracking of Facebook users with the pages they browse on)
or any other kind of "phoning home" functionality,
but they could of course have implemented this natively
via the WebView's
View.OnScrollChangeListener
or
View.OnClickListener,
as they
did
for long clicks for the FbQuoteShareJSInterface.
That being said, if after reading this you prefer your links to open in your default browser,
it's well hidden, but definitely possible: Settings > Media and Contacts > Links open externally.
It goes without saying, but just in case:
all code snippets in this post are owned by and copyright of Facebook.
Did you run a similar analysis with similar (or maybe different) findings?
Let me know on Twitter or Mastodon by posting your thoughts with a link to this post.
It will then show up as a Webmention at the bottom.
On supporting platforms, you can simply use the "Share Article" button below.
When it comes to animating SVGs, there're three options: using
CSS,
JS, or
SMIL.
Each comes with its own pros and cons, whose discussion is beyond the scope of this article,
but Sara Soueidan has a great
article on the topic.
In this post, I add a repeating shrink animation to a circle with all three methods,
and then try to use these SVGs as favicons.
Here's an example of animating an SVG with CSS based on the
animation and the
transform properties.
I scale the circle from the center and repeat the animation forever:
The SVG <script>
tag allows to add scripts to an SVG document.
It has some subtle differences
to the regular HTML <script>, for example, it uses the href instead of the src attribute,
but above all it's important to know that any functions defined within any <script> tag
have a global scope across the entire current document.
Below, you can see an SVG script used to reduce the radius of the circle until it's equal to zero,
then reset it to the initial value, and finally repeat this forever.
<svgviewBox="0 0 100 100"xmlns="http://www.w3.org/2000/svg"> <circlefill="blue"cx="50"cy="50"r="45"/> <scripttype="text/javascript"><![CDATA[ const circle = document.querySelector('circle'); let r =45; constanimate=()=>{ circle.setAttribute('r', r--); if(r ===0){ r =45; } requestAnimationFrame(animate); }; requestAnimationFrame(animate); ]]></script> </svg>
The last example uses SMIL, where, via the
<animate>
tag inside of the <circle> tag, I declaratively describe that I want to
animate the circle's r attribute (that determines the radius) and repeat it indefinitely.
Before using animated SVGs as favicons, I want to briefly discuss
how you can use each of the three examples on a website.
Again there're three options: referenced via the src attribute of an <img> tag,
in an <iframe>, or inlined in the main document.
Again, SVG scripts have access to the global scope, so they should definitely be used with care.
Some user agents, for example, Google Chrome, don't run scripts for SVGs in <img>.
The Glitch embedded below shows all variants in action.
My recommendation would be to stick with CSS animations whenever you can,
since it's the most compatible and future-proof variant.
Since crbug.com/294179 is fixed, Chrome finally supports SVG favicons,
alongside many other browsers.
I have recently successfully experimented with
prefers-color-scheme in SVG favicons,
so I wanted to see if animated SVGs work, too.
Long story short, it seems only Firefox supports them at the time of writing,
and only favicons that are animated with either CSS or JS.
You can see this working in Firefox in the screencast embedded below.
If you open my Glitch demo in a standalone window,
you can test this yourself with the radio buttons at the top.
Should you use this in practice?
Probably not, since it can be really distracting.
It might be useful as a progressive enhancement to show activity during a short period of time,
for example, while a web application is busy with processing data.
Before considering to use this, I would definitely recommend taking the user's
prefers-reduced-motion
preferences into account.