การฝัง CSS มีประสิทธิภาพมากขึ้นด้วย CSSNestedDeclarations

เผยแพร่: 8 ต.ค. 2024

เพื่อแก้ไขปัญหาแปลกๆ บางประการเกี่ยวกับการฝัง CSS กลุ่มทํางาน CSS จึงตัดสินใจเพิ่มอินเทอร์เฟซ CSSNestedDeclarations ลงในข้อกําหนดเฉพาะเกี่ยวกับการฝัง CSS การเพิ่มนี้ทำให้ประกาศที่อยู่หลังกฎรูปแบบไม่เลื่อนขึ้นอีกต่อไป รวมถึงการปรับปรุงอื่นๆ

การเปลี่ยนแปลงเหล่านี้พร้อมใช้งานใน Chrome ตั้งแต่เวอร์ชัน 130 และพร้อมให้ทดสอบใน Firefox Nightly 132 และ Safari Technology Preview 204

การรองรับเบราว์เซอร์

  • Chrome: 130
  • Edge: 130
  • Firefox: 132
  • Safari: ไม่รองรับ

ปัญหาการฝัง CSS โดยไม่ใช้ CSSNestedDeclarations

หนึ่งในข้อผิดพลาดที่มีการซ้อน CSS คือ ข้อมูลโค้ดต่อไปนี้ไม่ทำงานตามที่ควรจะเป็นในตอนแรก

.foo {
    width: fit-content;

    @media screen {
        background-color: red;
    }
    
    background-color: green;
}

เมื่อดูโค้ด คุณอาจคิดว่าองค์ประกอบ <div class=foo> มี green background-color เนื่องจากประกาศ background-color: green; อยู่ท้ายสุด แต่ Chrome ก่อนเวอร์ชัน 130 จะไม่แสดงข้อความนี้ ในเวอร์ชันเหล่านั้นที่ไม่รองรับ CSSNestedDeclarations background-color ขององค์ประกอบจะเป็น red

หลังจากแยกวิเคราะห์กฎจริง Chrome ก่อนเวอร์ชัน 130 จะใช้รูปแบบต่อไปนี้

.foo {
    width: fit-content;
    background-color: green;

    @media screen {
        & {
            background-color: red;
        }
    }
}

CSS หลังจากแยกวิเคราะห์จะมีการเปลี่ยนแปลง 2 อย่างดังนี้

  • background-color: green; เลื่อนขึ้นเพื่อรวมเข้ากับประกาศอีก 2 รายการ
  • CSSMediaRule ที่ฝังอยู่ได้รับการเขียนใหม่เพื่อตัดการประกาศใน CSSStyleRule เพิ่มเติมโดยใช้ตัวเลือก &

การเปลี่ยนแปลงทั่วไปอีกอย่างหนึ่งที่คุณจะเห็นที่นี่คือโปรแกรมแยกวิเคราะห์จะทิ้งพร็อพเพอร์ตี้ที่ไม่รองรับ

คุณตรวจสอบ "CSS หลังจากแยกวิเคราะห์" ด้วยตนเองได้โดยอ่าน cssText จาก CSSStyleRule

ลองใช้เองในพื้นที่ทดสอบแบบอินเทอร์แอกทีฟนี้

Why is this CSS rewritten?

หากต้องการทําความเข้าใจสาเหตุที่เกิดการเขียนใหม่ภายในนี้ คุณต้องเข้าใจวิธีแสดง CSSStyleRule นี้ใน CSS Object Model (CSSOM)

ใน Chrome ก่อนเวอร์ชัน 130 ข้อมูลโค้ด CSS ที่แชร์ก่อนหน้านี้จะเปลี่ยนรูปแบบเป็นดังนี้

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .resolvedSelectorText = ".foo"
  .specificity = "(0,1,0)"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ CSSMediaRule
    .type = MEDIA_RULE
    .cssRules (CSSRuleList, 1) =
      ↳ CSSStyleRule
        .type = STYLE_RULE
        .selectorText = "&"
        .resolvedSelectorText = ":is(.foo)"
        .specificity = "(0,1,0)"
        .style (CSSStyleDeclaration, 1) =
          - background-color: red

จากพร็อพเพอร์ตี้ทั้งหมดที่ CSSStyleRule มี 2 รายการต่อไปนี้มีความเกี่ยวข้องในกรณีนี้

ข้อมูลจึงสูญหายเนื่องจากการประกาศทั้งหมดจากข้อมูลโค้ด CSS ไปอยู่ในพร็อพเพอร์ตี้ style ของ CSStyleRule เมื่อดูที่พร็อพเพอร์ตี้ style จะไม่ชัดเจนว่ามีการประกาศ background-color: green หลัง CSSMediaRule ที่ฝังอยู่

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ …

ซึ่งทำให้เกิดปัญหา เนื่องจากการที่เครื่องมือ CSS จะทำงานได้อย่างถูกต้องได้นั้น เครื่องมือต้องแยกแยะพร็อพเพอร์ตี้ที่ปรากฏขึ้นที่จุดเริ่มต้นของเนื้อหากฎรูปแบบออกจากพร็อพเพอร์ตี้ที่ปรากฏสลับกับกฎอื่นๆ

สำหรับการประกาศภายใน CSSMediaRule ก็ถูกรวมไว้ใน CSSStyleRule อย่างกะทันหัน นั่นก็เพราะ CSSMediaRule ไม่ได้ออกแบบมาให้มีการประกาศ

เนื่องจาก CSSMediaRule อาจมีกฎที่ฝังอยู่ซึ่งเข้าถึงได้ผ่านพร็อพเพอร์ตี้ cssRules ประกาศจึงได้รับการตัดขึ้นบรรทัดใหม่ใน CSSStyleRule โดยอัตโนมัติ

↳ CSSMediaRule
  .type = MEDIA_RULE
  .cssRules (CSSRuleList, 1) =
    ↳ CSSStyleRule
      .type = STYLE_RULE
      .selectorText = "&"
      .resolvedSelectorText = ":is(.foo)"
      .specificity = "(0,1,0)"
      .style (CSSStyleDeclaration, 1) =
        - background-color: red

วิธีแก้ปัญหานี้

กลุ่มทํางาน CSS ได้พิจารณาตัวเลือกหลายอย่างในการแก้ปัญหานี้

หนึ่งในวิธีแก้ปัญหาที่แนะนำคือการรวมประกาศแบบเปลือยทั้งหมดไว้ใน CSSStyleRule ที่ฝังไว้ด้วยตัวเลือกการฝัง (&) แต่เราไม่ได้นำไปใช้เนื่องจากเหตุผลหลายประการ รวมถึงผลข้างเคียงที่ไม่พึงประสงค์ต่อไปนี้ของการแปลง & ให้เป็น :is(…)

  • ซึ่งส่งผลต่อความเฉพาะเจาะจง เนื่องจาก :is() ยึดความเฉพาะเจาะจงของอาร์กิวเมนต์ที่เฉพาะเจาะจงที่สุด
  • แต่จะทำงานได้ไม่ดีกับองค์ประกอบจำลองในตัวเลือกภายนอกเดิม เนื่องจาก :is() ไม่ยอมรับองค์ประกอบจำลองในอาร์กิวเมนต์รายการตัวเลือก

ลองดูตัวอย่างต่อไปนี้

#foo, .foo, .foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    background-color: green;
  }
}

หลังจากแยกวิเคราะห์ ข้อมูลโค้ดดังกล่าวจะกลายเป็นดังนี้ใน Chrome เวอร์ชันก่อน 130

#foo,
.foo,
.foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    & {
      background-color: green;
    }
  }
}

ปัญหานี้เกิดขึ้นเนื่องจาก CSSRule ที่ฝังอยู่ภายในตัวเลือก &

  • ยุบเป็น :is(#foo, .foo) โดยทิ้ง .foo::before ออกจากรายการตัวเลือกไป
  • มีความเฉพาะเจาะจง (1,0,0) ซึ่งทำให้เขียนทับได้ยากขึ้นภายหลัง

คุณสามารถตรวจสอบได้โดยดูว่ากฎจัดรูปแบบเป็นอะไร

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = "#foo, .foo, .foo::before"
  .resolvedSelectorText = "#foo, .foo, .foo::before"
  .specificity = (1,0,0),(0,1,0),(0,1,1)
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: red
  .cssRules (CSSRuleList, 1) =
    ↳ CSSMediaRule
      .type = MEDIA_RULE
      .cssRules (CSSRuleList, 1) =
        ↳ CSSStyleRule
          .type = STYLE_RULE
          .selectorText = "&"
          .resolvedSelectorText = ":is(#foo, .foo, .foo::before)"
          .specificity = (1,0,0)
          .style (CSSStyleDeclaration, 1) =
            - background-color: green

ซึ่งหมายความว่า background-color ของ .foo::before คือ red ไม่ใช่ green

อีกแนวทางหนึ่งที่กลุ่มทํางาน CSS พิจารณาคือให้คุณตัดการประกาศที่ฝังไว้ทั้งหมดไว้ในกฎ @nest ฟีเจอร์นี้ถูกปิดเนื่องจากประสบการณ์ของนักพัฒนาแอปจะแย่ลง

ขอแนะนําอินเทอร์เฟซ CSSNestedDeclarations

โซลูชันที่กลุ่มทํางาน CSS ตกลงใช้คือการใช้กฎการประกาศที่ซ้อนกัน

กฎประกาศที่ฝังอยู่นี้ใช้ใน Chrome ตั้งแต่ Chrome 130 เป็นต้นไป

การรองรับเบราว์เซอร์

  • Chrome: 130
  • Edge: 130
  • Firefox: 132
  • Safari: ไม่รองรับ

การใช้กฎประกาศที่ฝังอยู่จะเปลี่ยนโปรแกรมแยกวิเคราะห์ CSS ให้ตัดประกาศที่ฝังอยู่โดยตรงตามลำดับในอินสแตนซ์ CSSNestedDeclarations โดยอัตโนมัติ เมื่อแปลงเป็นอนุกรม อินสแตนซ์ CSSNestedDeclarations นี้จะปรากฏในพร็อพเพอร์ตี้ cssRules ของ CSSStyleRule

ลองดู CSSStyleRule ต่อไปนี้เป็นตัวอย่างอีกครั้ง

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

เมื่อจัดรูปแบบเป็นอนุกรมใน Chrome เวอร์ชัน 130 ขึ้นไป ข้อมูลจะมีลักษณะดังนี้

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .resolvedSelectorText = ".foo"
  .specificity = (0,1,0)
  .style (CSSStyleDeclaration, 1) =
    - width: fit-content
  .cssRules (CSSRuleList, 2) =
    ↳ CSSMediaRule
      .type = MEDIA_RULE
      .cssRules (CSSRuleList, 1) =
        ↳ CSSNestedDeclarations
          .style (CSSStyleDeclaration, 1) =
            - background-color: red
    ↳ CSSNestedDeclarations
      .style (CSSStyleDeclaration, 1) =
        - background-color: green

เนื่องจากกฎ CSSNestedDeclarations จบลงที่ CSSRuleList โปรแกรมแยกวิเคราะห์จึงเก็บตำแหน่งของประกาศ background-color: green ไว้ได้ ซึ่งก็คือหลังประกาศ background-color: red (ซึ่งเป็นส่วนหนึ่งของ CSSMediaRule)

นอกจากนี้ การมีอินสแตนซ์ CSSNestedDeclarations จะไม่ทำให้เกิดผลข้างเคียงที่ไม่ดีซึ่งโซลูชันอื่นๆ ที่ตอนนี้ถูกทิ้งไปแล้วทำให้เกิดขึ้น กฎประกาศที่ฝังอยู่จะจับคู่กับองค์ประกอบและองค์ประกอบจำลองเดียวกันกับกฎสไตล์หลัก โดยมีลักษณะการทำงานที่เฉพาะเจาะจงเหมือนกัน

หลักฐานที่ยืนยันเรื่องนี้คือการอ่าน cssText ของ CSSStyleRule เนื่องจากกฎการประกาศที่ซ้อนกันจึงเหมือนกับ CSS อินพุต ดังนี้

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

นโยบายนี้สำคัญกับคุณอย่างไร

ซึ่งหมายความว่าการฝัง CSS มีประสิทธิภาพมากขึ้นมากใน Chrome 130 แต่ก็หมายความว่าคุณจะต้องตรวจสอบโค้ดบางส่วนหากแทรกแซงการประกาศเปล่าด้วยกฎที่ซ้อนกัน

ลองดูตัวอย่างต่อไปนี้ที่ใช้ @starting-style ที่ยอดเยี่ยม

/* This does not work in Chrome 130 */
#mypopover:popover-open {
  @starting-style {
    opacity: 0;
    scale: 0.5;
  }

  opacity: 1;
  scale: 1;
}

ก่อน Chrome 130 ระบบจะยกระดับประกาศเหล่านั้น ผลลัพธ์ที่ได้คือประกาศ opacity: 1; และ scale: 1; จะไปอยู่ใน CSSStyleRule.style ตามด้วย CSSStartingStyleRule (แสดงกฎ @starting-style) ใน CSSStyleRule.cssRules

ตั้งแต่ Chrome 130 เป็นต้นไป ระบบจะไม่ยกระดับการประกาศอีกต่อไป และคุณจะเห็นออบเจ็กต์ CSSRule 2 รายการที่ฝังอยู่ใน CSSStyleRule.cssRules ตามลําดับคือ CSSStartingStyleRule 1 รายการ (แสดงกฎ @starting-style) และ CSSNestedDeclarations 1 รายการที่มีประกาศ opacity: 1; scale: 1;

เนื่องจากการทํางานที่เปลี่ยนแปลงนี้ การประกาศ @starting-style จะถูกเขียนทับโดยประกาศที่อยู่ในอินสแตนซ์ CSSNestedDeclarations จึงนําภาพเคลื่อนไหวของรายการออก

ในการแก้ไขโค้ด ให้ตรวจสอบว่าการบล็อก @starting-style ปรากฏขึ้นหลังจากการประกาศตามปกติ อย่างเช่นดังนี้

/* This works in Chrome 130 */
#mypopover:popover-open {
  opacity: 1;
  scale: 1;

  @starting-style {
    opacity: 0;
    scale: 0.5;
  }
}

หากคุณเก็บประกาศที่ฝังไว้ไว้ด้านบนของกฎที่ฝังไว้เมื่อใช้การฝัง CSS โค้ดจะทำงานได้ส่วนใหญ่กับเบราว์เซอร์ทุกเวอร์ชันที่รองรับการฝัง CSS

สุดท้ายนี้ หากต้องการตรวจหาความพร้อมใช้งานของ CSSNestedDeclarations ให้ใช้ข้อมูลโค้ด JavaScript ต่อไปนี้

if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
  // CSSNestedDeclarations is not available
}