Start with a need
This title is a bit of a tagline, but the fact is that I was recently working on an in-house tool and came across a requirement: given the URL of an image on a static resource site (such as a.xxxcdn.com/demo.jpg), how do you get the storage size and calculate the average Internet speed to load that resource? Note that it is the storage size, not the width and height of the image. Here are some of the ways I’ve summarized implementing this requirement.
First, obtain via Ajax request
This approach involves a property of XMLHttpRequest: responseType, which is standard in XMLHttpRequest Level2. ResponseType has several types: text, blob, ArrayBuffer, document. The default is to use the text type, which is the responseText we can get from the responseText in the return result to the body data returned by the server. If you set the responseText to a different type (e.g. blob), you will find that the responseText is not available as shown in the responseText file:
Now that the XMLHttpRequest object can help us convert the returned result, we can use it to do this. Let’s see what happens when we convert to an ArrayBuffer:
Change to ArrayBuffer
As you can see from the above figure, an object of type ArrayBuffer is retrieved from the Response field of the XMLHttpRequest instance object, which is used to represent a generic, fixed-length buffer of raw binary data. It exposes byteLength, which represents the size of the array in bytes, that is, B (1KB = 1024B), hence the storage size of the file is 14896B.
**[[Int8Array]] and [[Uint8Array]]** (Uint8Array)
First, we can convert this ArrayBuffer object into a Uint8Array type array using Uint8Array(Request.response). So why is the length the same? Because the byteLength of an ArrayBuffer represents its size in bytes, it is well known that a byte (1B) is composed of eight bits, such as 01010101. However, each value in the Uint8Array array represents the value after 8 binary bits are converted to decimal, so the corresponding Uint8Array has as many values as there are bytes in the ArrayBuffer. The same goes for Int8Array. The difference is that Int8Array is a signed integer and Uint8Array is unsigned.
Convert to Blob type
If you set responseType to BLOB, you can expect the XMLHttpRequest object to convert the result to bloB type, as shown below:
From its size, we can also get the correct byte size of the file. Also, it should be noted that bloBs and ArrayBuffers can be converted to each other using HTML5 FileReader, as you can find out in this article.
summary
At this point, this requirement can be achieved, but, through Ajax means to request a file, but also the server CORS configuration allows cross-domain, but the general server will not set access-Control-allow-Origin for *****, otherwise arbitrary which domain can request its resources. Therefore, we can imagine that img requests can not support cross-domain? Unfortunately, the img onLoad callback does not provide information about the size of the image file. It only provides information about the image elements themselves, such as the attributes in the wide and high HTMLImageElement.
After a few stackOverflow/MDN searches, we found a more recommended way to load images from img and fetch them from the Performance API.
Through the Performance API
Typically, performance timing can be used to measure various performance metrics of a page, such as DNS query time, HTTP connection time, first byte time, interactive time, etc. But performance also provides the ability to Allows us to probe the metrics of a loaded resource:
According to the MDN document, we can know that Performace API automatically generates PerformanceEntry for each resource when the browser loads the resource. There are generally three types of entrytypes of PerformanceEntry that are automatically generated: Resource, Navigation, and paint.
For images, CSS and script resource file, its entryType is resource, and at the same time is the expanded the PerformanceEntry PerformanceResourceTiming interface, The properties provide data about the size of the resource retrieved and the type of resource retrieved at initialization. For example, an image resource corresponding PerformanceResourceTiming object, will contain the following properties (part) :
As you can see, the last three attributes can represent the size of the resource. We choose encodedBodySize to represent the storage size of the resource, because this value is consistent with the size of the resource displayed in the Network panel of the browser. Let’s do it in code.
We use img to load a resource file, and the use of performance in the onload callback API to obtain its PerformanceResourceTiming:
var img = new Image();
var resource = 'https://a.xx.com/xxx.png';
img.src = resource;
img.onload = function() {
console.log(performance.getEntriesByName(resource));
}
Copy the code
The results are as follows:
Contrary to expectations, the three attributes that identify the size of the resource all return 0, but when I changed another image on another server, the three attributes returned the expected value:
Why do some things work and others don’t? After careful comparison, I found the difference between them. Every resource whose size can be detected has a field in the Response Header: timing-allow-origin: *. What does timing- allow-Origin do? Here I give an explanation of the MDN document:
The response header timing-allow-origin is used to specify a particular site to Allow access to information provided by the Resource Timing API that would otherwise be reported as zero due to cross-source restrictions
The script of the specified domain name can perform performance detection only for resources with the response header configured.
summary
At this point we can compare the two methods:
Both methods can successfully obtain the exact size of the resource storage bytes, but the former through Ajax request, the server needs to cooperate with the setting of CORS access-Control-Allow-Origin, but for a server, for security reasons, it is unlikely to set this field to *.
However, the second method also requires the server to set the timing-allow-origin field, but the difference is that this field is only used for performance detection, almost no security cost. However, I also found one using Timing -allow-origin on Google: * to detect the return time of the interface and infer the state of the interface. Therefore, the best way to avoid this vulnerability is to set the response header only on the server where the resource file is located, or add the response header to the request for the resource file on the primary server.
And, similarly, for other resources, such as fonts, style files, script files, etc., you can also use the above method to test the storage size.
Extended (to the full word) – about the cross-domain ability of pictures
Img and Script are known to support cross-domain access, which is a common capability provided by browsers. But have you ever thought that image requests and AJAX requests made by XMLHttpRequest are normal HTTP requests? Why can img and script bypass the same origin policy? We can look at the request headers for two cross-domain resource requests.
One is a request made with img:
One is the XMLHttpRequest get request:
Accept simply tells the server what type of content the client can Accept back, while Origin is the key to triggering the browser’s same-origin policy. After the browser receives the response from the request, it decides whether to Allow the user to read the return value by determining whether the access-Control-Allow-Origin field exists in the response header and verifying that it matches the Origin of the current request. If there is no access-Control-Allow-Origin field, we will see a very common browser error:
On the other hand, a request made by IMG that does not carry the Origin field will be ignored by the browser, thus bypassing the same Origin policy.