preface

We need to develop a multi-column infinite scrolling function, take watermelon video as an example:

For layout, flex is the first thing that comes to mind.

Here we use the 7-column 1228px container as an example to quickly write the following React code

import React, { useState } from "react";

const counts = 8;
function genRandomColor() {
  const fn = () = > parseInt(Math.random() * (255 + 1), 10);
  return `rgb(${fn()}.${fn()}.${fn()}) `;
}
function Item({ item }) {
  const { id, color } = item;
  return (
    <div
      style={{
        width: `calc(100% / 7) `,height: "40px",
        backgroundColor: color
      }}
    >
      {id}
    </div>
  );
}
export default function App() {
  const [items] = useState(() = >
    new Array(counts)
      .fill(1)
      .map((_, i) = > ({ id: `node${i + 1}`.color: genRandomColor() }))
  );
  return (
    <div style={{ width: 1228.display: "flex", flexWrap: "wrap}} ">
      {items.map((item) => (
        <Item key={item.id} item={item}></Item>
      ))}
    </div>
  );
}

Copy the code

See Codesandbox for the online demo

Open Chrome to see, HMM, the effect is no problem, test!

Shortly after, QA came in and said that the layout on Edge was out of order… Each row has only 6 columns instead of 7, and Node7 is displayed on the next row! The effect is as follows:

The one with Edge can be tested. There is indeed a problem. Note that Edge in this article refers to the lower version of Edge, that is, non-Chromium kernel; The new Edge uses the same rendering engine as Chrome and the results are the same

“Reasonable writing should be no problem, I see many other websites at home and abroad is such a layout? “I checked the YouTube home page and found that sometimes on Edge (say 2560px resolution) there was a line break with a blank space to the right, as shown in the image below, similar to our example above

The preliminary analysis

For the width of each item, the expected calculation result is 1228/7=175.42857142857142

Next, look at the width size for each browser on Computed TAB on the right side of the Elements panel

  • For Chrome 84, Computed Width shows 175.422
  • Edge 14, 175.48 in Computed Width
  • In Safari 13.1, Computed Width shows 175.42
  • Firefox 79, Computed Width shows 175.417

PS: All the experiments in this article tested these four browsers

Among them, only the width on Edge is greater than 1228/7, so item 7 cannot be fitted

Why are these decimal values different?

Background knowledge

Before we dive into this, let’s introduce some basic concepts

subpixel

A pixel is the smallest unit in an imaging system, which means that two pixels are connected macroscopically

But numerically, they can also exist in smaller units, which we call subpixels.

Subpixel values are calculated by subdivision of pixels (e.g., continuing to divide into 4×4), data interpolation and other subdivision algorithms.

Therefore, the width of the decimal point in the page will be represented as a sub-pixel, whose value can be obtained by pixel subdivision algorithm

For more details:

  • Sub Pixel
  • Subpixel rendering

(I still have a question here, though: what if you know the subpixel representation? Isn’t the smallest display unit on the screen still a pixel? Might be useful in some edge detection?

There seems to be no standard for subpixel size. According to Tapida, Chrome and Safari use 1/64 and Firefox uses 1/60. For IE/Edge, it’s 1/100

The value of the CSS

CSS 2.2 and CSS 3 have corresponding definitions for “value”

  • CSS 2.2 – Specified, computed, and actual values
  • css3 – Value Processing

The difference is that the value processing steps of the former are four, while the latter is six. Here we directly according to the latter latest specification to describe

  1. Collect Declared Values, which can be 0 or more, as defined in different stylesheets

  2. The Cascaded Values are obtained after the list of declared Values is prioritized according to certain rules

  3. Specified Values Specified Values = cascade value | | the default Values. The inherited property is inherit. The non-inherited property is initial. You can also explicitly set the following keywords: initial/inherit/unset

  4. The specified Values are parsed to obtain Computed Values. Computed values resolve the specified values as much as possible, but do not lay out the document. For example, font size can be determined before layout, so VW, REM equivalent will be parsed to PX, and other calC will be parsed as much as possible.

For historical reasons (I don’t know what it is, the documentation doesn’t say, but there should be something that shows the use value, and a computed value like auto doesn’t make sense), getComputedStyle doesn’t necessarily get a computed value, it can also get a used value. The style values that are displayed on DevTools are the ones that you get from getComputedStyle even if the property doesn’t apply

  1. The calculated Values are further evaluated in the document Layout to obtain the Used Values. As the name implies, this value is really used in the layout

For example, calculate the value as auto or percentage, and get the use value of 100px during layout. Or flex properties for flex element, did not use value for width and other properties, the use value is equal to the dom. GetBoundingClientRect (). The width, and the difference between calculated value rounding relationship, see below “decimal precision”

  1. Usage values can in principle be used directly, but user agents may not be able to use them in a given environment. For example, only integer pixel borders can be rendered, so the values used need to be rounded; Or the font size of the element may need to be adjusted based on font availability. The resulting Values after such adjustments are called Actual Values

For example

Property Declared value Cascading value Specify a value Calculated value Using value The actual value
width width: 80% 80% 80% 80% 354.2 px. 354px
font-size The font – size: 1.2 em 1.2 em 1.2 em 14.1 px. 14.1 px. 14px

More examples: www.w3.org/TR/css-casc…

On the actual value side, we may encounter an interesting phenomenon, that is, the same used value, but the actual value changes

Here I take offsetWidth as the actual value to understand, not sure whether correct, welcome to discuss

Again, the demo above, we can see that

document.querySelector("#root > div > div:nth-child(1)").offsetWidth / / 175
document.querySelector("#root > div > div:nth-child(2)").offsetWidth / / 176.Copy the code

Specific reasons have been analyzed by relevant partners, see details

  • Rem produced the problem of fractional pixels
  • 1 px disappear
  • LayoutUnit

The effect is:

  1. Elements are rounded to their actual values when rendered, so it can often be the case that the background image is cropped
  2. The space occupied is still the original size, that is, using values, so the total width of the element remains the same
  3. The actual value of the following elements will be completed or deducted based on the rounding of the previous element, resulting in the actual value may be different from the previous one

Decimal point accuracy test

As we know above, CSS uses sub-pixel technology to preserve decimal places.

So how many digits are reserved? What about the percentages that participate in the calculation?

Test with the following cases

  1. Percentage explicitly set, as in14.285714285714286%How many digits do I take? Or do you just round off the results at the end?
  2. Explicitly set decimal pixels, as in210.123456789123456789 px.How many digits do I take?
  3. Calc does percentage math, for examplecalc(100%/7)How many digits will I take?
  4. In calC, in addition to percentage math, there are other pixel operations, such asCalc (100% / 7-0.019)How many digits are we going to take?

For demo, see TEST2: Testing decimal accuracy

The following test results are obtained:

Chrome:

Safari:

Firefix:

Edge:

Group 1 and 2: Verify rounding of percentage decimals (2, 4, 13 digits)

The first group takes the value of < 5 in the 3rd, 5th and 14th decimal places, and the second group takes the value of > 5

This experiment is based on the subpixel rendering versus decimal trade-off in the article browser, but my conclusion here is different

The percentages on Edge are rounded down by 2 decimal places, and the final result continues to be rounded down by 2 decimal places

1228/100 * 50.42 = 619.1576= >619.15(Same as test results)1228/100 * 50.56 = 620.8768= >620.87(Same as test results)1228/100 * 50.57 = 620.9996
Copy the code

No matter what kind of decimal carry you use for Chrome/Safari/Firefox, you can’t get the corresponding result

1228/100 * 50.4234 = 619.199352 
1228/100 * 50.5698 = 620.997143
Copy the code

But subpixel theory makes sense.

1/64 = 0.015625
1/60 = 0.016666666666666666

12/64 = 0.1875
11/60 = 0.18333333333333332

63/64 = 0.984375
59/60 = 0.9833333333333333
Copy the code

Conclusion of Experiment 1 and 2

Chrome/Safari/Firefox are calculated with enough decimal places (i.e. not rounded), and the resulting decimal places are then subpixels rounded down in basic units such as 1/64 or 1/60 to match the test results

Firefox doesn’t divide all 1/60, so there may be some differences in the next few decimal places, but it’s always less than what we calculated manually

Other conclusions can be drawn:

  • Firefox and Chrome both calculate values by rounding the values used to keep 3 decimal places
  • Safari calculates the same value as it uses, i.egetComputedStylegetBoundingClientRectThe same value
  • Firefox reserves 14 decimal places in “Use value” because of the number of subpixels, while Chrome/Safari only reserves 6 decimal places
  • The “used value” of Edge is reserved for 14 decimal places, while the calculated value is reserved for 2 decimal places. The difference between the two values < 1E-4 is ignored, which can be understood as the special processing of Edge sub-pixel. Relevant documents have not been found yet
  • Of course none of this matters, we use “use values” for layout, and calculating values only affects the presentation of tools like DevTools

Due to rounding down, you can see that two elements greater than 50% can fit within 100%

For details, see Demo Experiment 2: Test that two widths greater than 50% can fit in 100%

Percentage rounding is the reason for Edge, while browsers like Chrome don’t reach a sub-pixel size

1228 * 0.0011/100 = 0.013508 < 1/64 = 0.015625
Copy the code

Group 3: rounding rules for decimal pixels

Chrome/Safari/Firefox still satisfies the subpixel rule, taking the subpixel base unit and rounding down

7/64 = 0.109375
7/60 = 0.11666666666666667
Copy the code

For Edge, it is rounded down by 2 decimal places

Groups 4 to 6: percentage processing rule in CALC

The results above are all rounded down, so they have nothing to do with overflow.

But here’s where it gets interesting: back to our original question: Why is calC computed on Edge too large

A routine Chrome/Safari/Firefox analysis found that it still meets the sub-pixel rule, but Edge?

calc(100% /7) = 175.48
calc(100% /7 - 10px) = 165.43
calc(100% /7 - 0px) = 175.43
Copy the code

Due to the

100/7 = 14.285714
1228/100 * 14.29 = 175.48119~ =175.48(to meet first4Set of results)1228/100 * 14.2857 = 175.428396~ =175.43(to meet first6Set of results)Copy the code

Let’s make a bold assumption:

  1. In calC, only percentages are calculated. In the “calculated value” phase, percentages are rounded to keep 2 decimal places
  2. For other numerical calculations, the “calculated value” phase, the percentages are rounded to 4 decimal places

We will verify this later and continue to analyze the following groups

Group 7 to 8: Set percentage rounding rules explicitly in CALC

Calc (14.2857%) = 175.35 CALc (14.2857% - 0px) = 175.36Copy the code

Due to the

1228/100 * 14.28 = 175.3584
Copy the code

We propose the following hypothesis:

  1. The percentage set explicitly in calc that is rounded down to preserve 2 decimal places if no other numeric term is computed
  2. If there are other numerical terms to calculate, round the result to keep 2 decimal places

Edge decimal point test

The performance on Edge is quite strange, and we analyze it separately, see TEST3: Edge Decimal Point Test

For easy understanding, we make the following definitions:

  1. Explicit percentage: indicates a percentage set directly, for example14.28%
  2. Implicit percentage: percentage divided by percentage, as in100% / 7
  3. Decimal pixel: refers to a pixel value that may contain decimal values, such as14.11 0 px, px
  4. Pixel value term: decimal pixels and operations on decimal pixels (e.g72px/7)
  5. Value items: including percentage and pixel value items

The test results are shown below

According to groups 1 and 2, the following conclusions can be drawn:

Explicit percentages, which are carried to the second decimal place only if the 3456 decimal place is greater than 9985; Otherwise, round down to keep 2 decimal places

The following conclusions can be drawn from groups 3 to 5

Explicit percentage and decimal pixels are preprocessed by rounding down to preserve 2 decimal places (except in the special case of group 2)

For rounding of computed results:

  • If there is only a single numeric entry (percentage or decimal pixels), the result is rounded down to keep 2 decimal places
  • If there are more than one numeric term, the result is rounded to 2 decimal places

According to groups 6 to 7, the following conclusions can be drawn

The associative law does not affect the calculation of numerical terms

Pixel value entries are rounded down to retain 2 decimal places, such as 72/7 = 10.2857 =>10.28

The following conclusions can be drawn from groups 8 to 12

The implicit percentage is calculated with the pixel value item. The number of pixel value items will affect the rounding rule of the implicit percentage:

  • If there is only one pixel value item, implicit percentage rounding preserves 4 decimal places
  • Otherwise (0, or 2 at most), the implicit percentage is rounded to keep 2 decimal places

The following conclusions can be drawn from groups 13 to 16

The conclusions of group 3~5 are as follows:

  • Explicit percentage numeric entries are rounded down first
  • For a single numeric item, the calculation results are rounded down
  • Multiple numerical items, the calculation results are rounded

The following conclusions can be drawn from groups 17 to 18

The rounding rule for implicit percentages is related only to the pixel value item, not the number of explicit percentages.

20200906 Added later

It occurs to me that if multiple pixel value items are combined by parentheses, they will be further processed in the “calculated value” stage and should be considered as a value item.

And the results proved me right

calc(100% / 7- (0px - 0px)) first20Group: getComputedStyle Value:175.43px; GetBoundingClientRect values:175.42999267578125Description:1228 * 0.142857 = 175.428396~ =175.43
Copy the code

That is to say, when parentheses are used, the operations between pixel value items are unified to calculate a pixel value item

calc(100% / 7 - (72px / 7 + 0.01px))
/ / is equivalent to
calc(100% / 7 - 10.29px)
Copy the code

Redefine the pixel value item: the item that has an operational relationship with the implicit percentage is counted as one item, and the item that is calculated first among multiple pixel value items is unified as one item

Edge synthesis conclusion

  1. For explicit percentage and pixel value entries, two decimal digits of 2 are rounded down

  2. For implicit percentages, the rounding rule is determined by the number of pixel value items

  3. Rounding continues to keep 2 decimal places, single value terms are rounded down, and multiple value terms are rounded off.

Edge Solutions

Back to the problem itself, Edge is because the total value of the rounding rule is larger than the container, resulting in overflow line breaks

Width: calc(100/7)

Actual value = (1228 * 0.1429).toFixed(2) = 175.48
175.48 * 7= = =1228.36 > 1228
Copy the code

The last item will not fit, so we need to subtract some rounding value

Assuming our container width = 1228, column number cols = 7, margin value between the columns is 0

If a rounding value is deducted later, the pixel value item numCounts = 2

const floor = (num, decimal = 0) = > {
    const expand = 10 ** decimal
    return Math.floor(num * expand) / expand
}
const getRealWidth = (width, cols, margins, numCounts) = > {
    const fixed = numCounts === 1 ? 6 : 4 // Keep four decimal places for a numeric type or two decimal places for a numeric type
    const tmp = width * (1 / cols).toFixed(fixed) - floor(margins / cols, 2)
    return Number(tmp.toFixed(2))}/ * * * *@param {*} width 
 * @param {*} cols 
 * @param {*} margins 
 * @param {*} NumCounts Number of pixel value items used to determine the rounding rule for implicit percentages */
const getDifference = (width, cols, margins, numCounts) = > {
    const calcWidth = (width - margins) / cols
    const realWidth = getRealWidth(width, cols, margins, numCounts)
    return {
        calcWidth,
        realWidth, // The actual rendered value of the browser
        difference: calcWidth - realWidth
    }
}

Copy the code

Based on the values above, we can get

getDifference(1228.7.0.2)
// {calcWidth: 175.42857142857142, realWidth: 175.48, difference: -0.05142857142857338}
Copy the code

Therefore, 0.06px can be deducted from the calculation results of each term

calc(100% /7 -0px -0.06px)
=>
(1228 * 0.1429).toFixed(2) -0.06  = 175.42
175.42 * 7 = 1227.94 < 1228
Copy the code

20200906 afterword.

We adjusted the calculation order of pixel value items to make the number of pixel value items numCounts become 1, so that the difference calculated would be smaller

getDifference(1228.7.0.1)
// {calcWidth: 175.42857142857142, realWidth: 175.43, difference: -0.0014285714285904305}
Copy the code

Take the difference = 1

calc(100% /7 -(0px + 0.01px))
/ / is equivalent to
calc(100% /7 - 0.01px)
=>
(1228 * 0.142857).toFixed(2) - 0.01  = 175.42000000000002
175.42000000000002 * 7 = 1227.94 < 1228
Copy the code

Looks like 0.01px is enough for everything, right? We verify:

const getRealWidth = (width, cols, margins, numCounts) = > {
    const fixed = numCounts === 1 ? 6 : 4 // Keep four decimal places for a numeric type or two decimal places for a numeric type
    const tmp = width * (1 / cols).toFixed(fixed) - floor(margins / cols, 2)
    return Number(tmp.toFixed(2))}Copy the code

The margins are set to below zero, with the margins being 72 and cols being 7,

Floor (margins/cols, 2) = floor(margins, 1, 0

Because margins and Cols can take on a variety of values, we’ll assume the limit is 0.0099

The other, implicit percentage calculation, calculates the percentage, the width is rounded to keep 6 decimal places, i.e. the limit is width * 0.0000005

If width=5120, the limit value is 0.00256

The final result can be a 0.02px offset because we may have added 0.0xx (0.0099+0.00256) to the original result

Conclusion:

  • Normally 0.01px is enough. If not, it is magins’ problem. You can calculate on the floor by yourself to see how much less it will be
  • If you want stability, keep setting it to 0.02px

Best practices

All of our projects are responsive processing, that is, displaying different numbers of cards at different resolutions.

If there are two pixel value items, we need to calculate the difference of each resolution and take the maximum difference

// Assume that the column numbers corresponding to different resolutions are:
$narrowItemRowCounts = {
  '1024px': 4.'1280px': 5.'1440px': 5.'1680px': 6.'1920px': 6.max: 7
}
// Column spacing is 12px
Copy the code

The largest difference can be calculated in this way (4 and 5 columns are not counted, because it is divisible, so the difference must be 0).

const floor = (num, decimal = 0) = > {
    const expand = 10 ** decimal
    return Math.floor(num * expand) / expand
}
const getRealWidth = (width, cols, margins, numCounts) = > {
    const fixed = numCounts === 1 ? 6 : 4 // Keep four decimal places for a numeric type or two decimal places for a numeric type
    const tmp = width * (1 / cols).toFixed(fixed) - floor(margins / cols, 2)
    return Number(tmp.toFixed(2))}/ * * * *@param {*} width 
 * @param {*} cols 
 * @param {*} margins 
 * @param {*} NumCounts Number of pixel value items used to determine the rounding rule for implicit percentages */
const getDifference = (width, cols, margins, numCounts) = > {
    const calcWidth = (width - margins) / cols
    const realWidth = getRealWidth(width, cols, margins, numCounts)
    return {
        calcWidth,
        realWidth, // The actual rendered value of the browser
        difference: calcWidth - realWidth
    }
}

$narrowItemRowCounts = {
    '1024px': 4.'1280px': 5.'1440px': 5.'1680px': 6.'1920px': 6.max: 7
}

let max = 0
let width = 0
for (let i = 1025; i <= 1440; i++) {
    let result = Math.abs(getDifference(i, 5.4 * 12.2).difference)
    if (result > max) {
        max = result
        width = i
    }
}
console.log(`1025~1440 cols:5,max difference: ${max} ,width:${width}`)
// 1025~1440 cols:5,max difference: 0 ,width:0
for (let i = 1441; i <= 1920; i++) {
    let result = Math.abs(getDifference(i, 6.5 * 12.2).difference)
    if (result > max) {
        max = result
        width = i
    }
}
console.log(`1441~1920 cols:6,max difference: ${max} ,width:${width}`)
for (let i = 1921; i <= 5120; i++) {
    let result = Math.abs(getDifference(i, 7.6 * 12.2).difference)
    if (result > max) {
        max = result
        width = i
    }
}
console.log(`1921~5120 cols:7,max difference: ${max} ,width:${width}`)

// 1025~1440 cols:5,max difference: 0 ,width:0
// cols:6, Max difference: 0.0666666666288,width:1853
// 1921-5120 COLs :7, Max difference: 0.2300000000000182,width:5119
Copy the code

It can be found that the difference is a little too big. If the difference is deducted, there will be a gap of 1 pixel (0.23*7) in the end.

It would be better to use a single pixel value term solution, as verified above, 0.01px works for most cases

Finally, a word about YouTube. As stated at the beginning of this article, YouTube is problematic at some resolutions because of the code inside it

Calc (100%/ 7-6 *12px/ 7-0.01px)Copy the code

Reducing 0.01px directly can be problematic in some cases and can still be problematic, so the solution in this article should be used.

  • Not combining pixel value terms requires deducting a large deviation value
  • To combine pixel value items, only subtract0.01 px.Can be, that is,Calc (100%/7 - (6*12px/7 + 0.01px))

conclusion

In terms of decimal pixel processing, browsers all use sub-pixel technology, but the details are different, with Firefox using 1/60 and Chrome/Safari using 1/64. At processing time, subpixels are rounded down, so they never exceed the parent container width.

In calc processing, Firefox/Chrome/Safari results always follow the sub-pixel rounding down principle. Edge can be rounded up, so we need to subtract a bias value from each element to ensure that the end result does not exceed the parent container width

Other information

In addition to the above articles, the following answers have also helped me:

  • CSS sets the width and height of elements to integers. Why do some browsers interpret the width and height as decimal? – Answer by Sun Beiji – Zhihu

www.zhihu.com/question/48…

  • CSS sets the width and height of elements to integers. Why do some browsers interpret the width and height as decimal? – Tapirs eat incense. – Zhihu

www.zhihu.com/question/48…

  • What if the browser has a precision error when converting REM to PX? – Tapirs eat incense. – Zhihu

www.zhihu.com/question/26…

  • What if the browser has a precision error when converting REM to PX? – Answer of the desert – Zhihu

www.zhihu.com/question/26…